Last active
May 4, 2019 08:32
-
-
Save CyberShadow/d15eefe8b30569988e1752e8717f4824 to your computer and use it in GitHub Desktop.
Terraria Compact Crafting Workstation Generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/arrange |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import std.algorithm.comparison; | |
import std.algorithm.iteration; | |
import std.algorithm.searching; | |
import std.parallelism; | |
import std.algorithm.comparison : min, max; | |
import std.range; | |
import std.random; | |
import std.stdio; | |
import std.array; | |
struct Item | |
{ | |
char character; | |
string name; | |
int width, height; | |
bool isPlatform; /// The player can stand on this item. | |
bool isFlat; /// Items with canStandOnFlat can be stacked directly on this item. | |
bool canStandOnFlat; /// Can be stacked on isFlat items. | |
bool click; /// Needs to be right-clicked, rather than just be in range | |
} | |
immutable Item[] items = | |
[ | |
Item('w', "Workbench" , 2, 1, true , true , false, false), // E.g. Wood -> 4x Wood Wall | |
Item('p', "Piggy Bank" , 2, 1, false, false, true , true ), // Right-click range | |
// Item('s', "Safe" , 2, 2, false, false, true , true ), // Right-click range | |
Item('a', "Anvil" , 2, 1, true , false, false, false), // E.g. 8x Iron Bar -> Iron Broadsword | |
Item('l', "Loom" , 3, 2, false, false, false, false), // E.g. 7x Cobweb -> Silk | |
Item('W', "Sawmill" , 3, 3, false, false, false, false), // E.g. 20x Wood -> Mannequin | |
Item('T', "Table" , 3, 2, true , true , false, false), // E.g. 10x Copper Bar + Chain -> Copper Watch | |
Item('c', "Chair" , 1, 2, false, false, false, false), // Ditto | |
// Item('D', "Dye Vat" , 3, 3, false, false, false, false), // E.g. Black Ink -> Black Dye | |
Item('b', "Bottle" , 1, 1, false, false, true , false), // E.g. Bottled Water + ... -> potion | |
Item('k', "Sink" , 2, 2, false, false, true , false), // E.g. Bottle -> Bottled Water | |
// Item('A', "Alchemy Table" , 3, 3, false, false, false, false), // E.g. Bottled Water + Daybloom + Blinkroot -> Night Owl Potion | |
Item('F', "Furnace" , 3, 2, false, false, false, false), // E.g. 3x Copper Ore -> Copper Bar | |
Item('C', "Cooking Pot" , 2, 2, false, false, false, false), // E.g. Bass -> Cooked Fish | |
// Item('L', "Living Loom" , 3, 3, false, false, false, false), // E.g. 4x Wood -> Living Wood Chair | |
// Item('I', "Imbuing Station" , 3, 3, false, false, false, false), // E.g. Bottled Water + Stinger -> Flask of Poison | |
// Item('H', "Heavy Workbench" , 3, 3, false, false, false, false), // E.g. 12x Wood + 4x Any Iron Bar -> Tall Gate | |
Item('K', "Tinkerer's Workshop" , 3, 2, true , true , false, false), // | |
// Item('S', "Solidifier" , 3, 3, false, false, false, false), // E.g. Slime -> Slime Block | |
]; | |
enum nItems = cast(ubyte)items.length; | |
enum playerWidth = 1; | |
enum playerHeight = 3; | |
// Distance between player and object. Objects at or above this distance are too far away. | |
enum craftRangeSide = 4; // must be 3 tiles away or closer | |
enum craftRangeUp = 0; // must be overlapping | |
enum craftRangeDown = 3; // must be 2 tiles below or closer | |
enum clickRangeSide = 5; // must be 4 tiles away or closer | |
enum clickRangeUp = 3; // must be 2 tiles above or closer | |
enum clickRangeDown = 3; // must be 2 tiles below or closer | |
enum maxCraftObjectWidth = items.filter!(item => !item.click).map!(item => item.width).reduce!max; | |
enum maxCraftObjectHeight = items.filter!(item => !item.click).map!(item => item.height).reduce!max; | |
enum maxClickObjectWidth = items.filter!(item => item.click).map!(item => item.width).reduce!max; | |
enum maxClickObjectHeight = items.filter!(item => item.click).map!(item => item.height).reduce!max; | |
// Size of placing area | |
// Coordinates of player's 1x3 rect (top-left corner) | |
enum playerX = | |
max( | |
maxCraftObjectWidth + craftRangeSide - 1, | |
maxClickObjectWidth + clickRangeSide - 1, | |
); | |
enum width = playerX + playerWidth + playerX; | |
enum playerY = | |
max( | |
maxCraftObjectHeight + craftRangeUp - 1, | |
maxClickObjectHeight + clickRangeUp - 1, | |
); | |
enum height = | |
playerY + | |
playerHeight + | |
max( | |
maxCraftObjectHeight + craftRangeDown - 1, | |
maxClickObjectHeight + clickRangeDown - 1, | |
) + | |
1 ; // Extra row for platforms | |
// Additional constraints | |
enum borderX = 0; | |
enum minX = borderX; | |
enum maxX = width - borderX + 1; | |
enum minY = 2; | |
enum maxY = height - 4; | |
struct State | |
{ | |
byte[width][height] grid = [[gridEmpty].replicate(width)].replicate(height); | |
enum gridEmpty = -1; | |
enum gridPlatform = -2; | |
bool placeItem(byte itemIndex) | |
{ | |
auto p = &items[itemIndex]; | |
int rangeSide = p.click ? clickRangeSide : craftRangeSide; | |
int x0 = playerX - rangeSide - p.width + 1; | |
int x1 = playerX + playerWidth + rangeSide - 1 + 1; | |
x0 = x0.max(minX); | |
x1 = x1.min(maxX - p.width); | |
int y0 = playerY - (p.click ? clickRangeUp : craftRangeUp ) - p.height + 1; | |
int y1 = playerY + playerHeight + (p.click ? clickRangeDown : craftRangeDown) - 1 + 1; | |
y0 = y0.max(minY); | |
y1 = y1.min(maxY - p.height); | |
scope(failure) writeln([x0, y0, x1, y1, width, height, playerX, playerY]); | |
static struct Coord { int x, y; } | |
Coord[width*height] coords; | |
int nCoords; | |
foreach (y; y0..y1) | |
coordSearch: | |
foreach (x; x0..x1) | |
{ | |
int checkHeight = p.height; | |
if (!p.canStandOnFlat) | |
checkHeight++; | |
foreach (j; 0..checkHeight) | |
foreach (i; 0..p.width) | |
if (grid[y+j][x+i] != gridEmpty) | |
continue coordSearch; | |
if (p.canStandOnFlat) | |
{ | |
foreach (i; 0..p.width) | |
{ | |
auto q = grid[y+p.height][x+i]; | |
if (q >= 0 && !items[q].isFlat) | |
continue coordSearch; | |
} | |
} | |
coords[nCoords++] = Coord(x, y); | |
} | |
if (!nCoords) | |
return false; | |
auto coord = coords[uniform(0, nCoords)]; | |
with (coord) | |
{ | |
foreach (j; 0..p.height) | |
foreach (i; 0..p.width) | |
grid[y+j][x+i] = itemIndex; | |
foreach (i; 0..p.width) | |
if (grid[y+p.height][x+i] == gridEmpty) | |
grid[y+p.height][x+i] = gridPlatform; | |
} | |
return true; | |
} | |
bool ok() | |
{ | |
// Can the player stand in the designated position? | |
foreach (int dx; -1 .. 1 + 1) | |
{ | |
auto x = playerX + dx; | |
auto y = playerY + playerHeight; | |
if (grid[y][x] == gridPlatform) | |
return true; | |
if (grid[y][x] != grid[y-1][x] && grid[y][x] >= 0 && items[grid[y][x]].isPlatform) | |
return true; | |
} | |
return false; | |
} | |
int score() | |
{ | |
int result = 100; | |
// Try to put the table and chair next to each other | |
static immutable itemNames = items.map!(item => item.name).array; | |
enum gridTable = itemNames.countUntil("Table"); | |
enum gridChair = itemNames.countUntil("Chair"); | |
static if (gridTable >= 0 && gridChair >= 0) | |
{ | |
tableChair: | |
foreach (x; 0 .. width-1) | |
foreach (y; 0 .. height-1) | |
if (grid[y ][x ] != grid[y ][x+1] && | |
grid[y ][x ] == grid[y+1][x ] && | |
grid[y ][x+1] == grid[y+1][x+1] && | |
grid[y ][x ].among(gridTable, gridChair) && | |
grid[y ][x+1].among(gridTable, gridChair)) | |
{ | |
result += 10; | |
break tableChair; | |
} | |
} | |
foreach (y; playerY .. playerY + playerHeight) | |
if (grid[y][playerX] == gridEmpty) | |
result += 2; | |
else | |
if (grid[y][playerX] == gridPlatform) | |
result += 1; | |
if (grid[playerY + playerHeight - 1][playerX] == gridEmpty) | |
result += 4; | |
// foreach (x; playerX - 1 .. playerX + 1 + 1) | |
// if (grid[playerY + playerHeight][x] == gridPlatform) | |
// result += 2; | |
// else | |
// if (grid[playerY + playerHeight][x] != grid[playerY + playerHeight - 1][x] && | |
// grid[playerY + playerHeight][x] >= 0 && items[grid[playerY + playerHeight][x]].isPlatform) | |
// result += 1; | |
// Make accidentally climbing higher than needed harder | |
{ | |
enum y = playerY + 2; | |
foreach (x; playerX - 1 .. playerX + 1 + 1) | |
if (grid[y][x] == gridPlatform || | |
( | |
grid[y][x] != grid[y - 1][x] && | |
grid[y][x] >= 0 && items[grid[y][x]].isPlatform | |
) | |
) | |
result -= 5; | |
} | |
return result; | |
} | |
void print() | |
{ | |
foreach (y; 0..height) | |
{ | |
foreach (x; 0..width) | |
{ | |
auto i = grid[y][x]; | |
if (i == gridEmpty) | |
write('·'); | |
else | |
if (i == gridPlatform) | |
write('‾'); | |
else | |
write(items[i].character); | |
} | |
if (y >= playerY && y < playerY + playerHeight) | |
write('<'); | |
writeln(); | |
} | |
foreach (x; 0..width) | |
{ | |
if (x >= playerX && x < playerX + playerWidth) | |
write('^'); | |
else | |
write(' '); | |
} | |
writeln(); | |
stdout.flush(); | |
} | |
} | |
void saveLegend() | |
{ | |
//auto f = File("legend.txt", "wb"); | |
auto f = stdout; | |
foreach (item; items) | |
f.writefln("%s - %s", item.character, item.name); | |
} | |
void main() | |
{ | |
version (Windows) { import win32.winbase; SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS); } | |
saveLegend(); | |
int bestScore; | |
searchLoop: | |
foreach (i; int.max.iota.parallel) | |
{ | |
State state; | |
static int bestScoreThread = 0; | |
ubyte[nItems] itemOrder; | |
foreach (ubyte n; 0..nItems) | |
itemOrder[n] = n; | |
randomShuffle(itemOrder[]); | |
foreach (n; itemOrder) | |
if (!state.placeItem(n)) | |
continue searchLoop; | |
if (!state.ok) | |
continue; | |
auto score = state.score; | |
if (bestScoreThread < score) | |
{ | |
bestScoreThread = score; | |
synchronized | |
{ | |
if (bestScore < score) | |
{ | |
bestScore = score; | |
state.print(); | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment