Last active
April 4, 2023 13:25
-
-
Save CyberShadow/156f1582b2fc8108e74b5e12965fccc0 to your computer and use it in GitHub Desktop.
Into The Breach solver
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
/problem.txt | |
/solver |
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 core.bitop : bsr; | |
import std.algorithm; | |
import std.array; | |
import std.conv; | |
import std.exception; | |
import std.format; | |
import std.math; | |
import std.parallelism; | |
import std.range; | |
import std.stdio; | |
import std.string; | |
static import std.random; | |
import ae.utils.exception; | |
import ae.utils.meta : enumLength; | |
struct Terrain | |
{ | |
char c; | |
string name; | |
bool solid; // can be walked on / through, stops projectiles | |
} | |
enum TerrainKind | |
{ | |
ground, | |
water, | |
mountain, | |
damagedMountain, | |
forest, | |
fire, | |
civilianBuilding, | |
emerging, | |
chasm, | |
timePod, | |
ice, | |
damagedIce, | |
mine, | |
sand, | |
smoke, | |
} | |
Terrain[] terrains = | |
[ | |
{ c : '.', name : "Ground Tile" , solid : false, }, | |
{ c : 'w', name : "Water Tile" , solid : false, }, | |
{ c : 'M', name : "Mountain Tile" , solid : true , }, | |
{ c : 'm', name : "Damaged Mountain" , solid : true , }, | |
{ c : 'f', name : "Forest Tile" , solid : false, }, | |
{ c : '!', name : "Forest Fire" , solid : false, }, | |
{ c : 'B', name : "Civilian Building", solid : true , }, | |
{ c : 'e', name : "Emerging" , solid : false, }, | |
{ c : 'c', name : "Chasm" , solid : false, }, | |
{ c : 't', name : "Time Pod" , solid : false, }, | |
{ c : 'I', name : "Ice Tile" , solid : false, }, | |
{ c : 'i', name : "Damaged Ice Tile" , solid : false, }, | |
{ c : 'l', name : "Old Earth Mine" , solid : false, }, | |
{ c : 's', name : "Sand" , solid : false, }, | |
{ c : 'S', name : "Smoke" , solid : false, }, | |
]; | |
enum EnvironmentKind | |
{ | |
none, | |
airSupport, | |
} | |
struct Environment | |
{ | |
char c; | |
string name; | |
} | |
Environment[] environments = | |
[ | |
{ c : ' ', name : "None" }, | |
{ c : 'A', name : "Air Support" }, | |
]; | |
struct Weapon | |
{ | |
string name; | |
byte damage = 0; | |
int minRange = 1; | |
int maxRange = 8; | |
bool artillery; // can fly over obstacles; distance is chosen | |
bool push; | |
bool pushSurrounding; | |
bool webbing; | |
bool charge; | |
bool buildingsImmune; | |
bool piercing; // causes damage to all cells between minRange and maxRange | |
bool collide; | |
bool leap; // unit is moved to attack target | |
bool allDirections; | |
byte uses = -1; | |
} | |
immutable Weapon[] weapons = | |
[ | |
// { name : "Titan Fist" , damage : 2 , maxRange : 1, push : true }, | |
{ name : "Titan Fist" , damage : 2 , push : true, charge : true }, | |
{ name : "Taurus Cannon" , damage : 1 , push : true }, | |
{ name : "Artemis Artillery" , damage : 1 , minRange : 2, artillery : true, pushSurrounding : true, buildingsImmune : true }, | |
{ name : "Astra Bombs" , damage : 1 , minRange : 2, artillery : true, leap : true, uses : 2 }, | |
{ name : "Stock Cannon" , push : true }, | |
{ name : "Choo Choo" , damage : 99, maxRange : 2, collide : true }, | |
{ name : "(explosive decay)" , damage : 1, maxRange : 1, allDirections : true }, // dummy | |
{ name : "Accelerating Thorax", damage : 1 }, | |
{ name : "Stinging Spinneret" , damage : 1, maxRange : 1, webbing : true }, | |
{ name : "Stinger" , damage : 1, maxRange : 1 }, | |
{ name : "Enhanced Thorax" , damage : 3 }, | |
{ name : "Pincers" , damage : 1, push : true, charge : true }, | |
{ name : "Goring Spinneret" , damage : 3, maxRange : 1, webbing : true }, | |
{ name : "Launching Stinger" , damage : 2, maxRange : 2, piercing : true }, | |
{ name : "Spitting Glands" , damage : 1, artillery : true }, | |
{ name : "Spitting Glands 2" , damage : 3, artillery : true }, | |
{ name : "Massive Spinneret" , damage : 2, maxRange : 1, webbing : true, allDirections : true }, | |
]; | |
int findWeapon(string name) { foreach (int i, w; weapons) if (w.name == name) return i; assert(false, "No such weapon: " ~ name); } | |
enum Party | |
{ | |
civilian, | |
player, | |
enemy, | |
volatile, // for score calculations only | |
} | |
enum PassiveBonus | |
{ | |
none, | |
invigoratingSpores, | |
explosiveDecay, | |
} | |
struct Unit | |
{ | |
char c; | |
string name; | |
Party party; | |
int maxHP; | |
int movement; | |
int weapon1 = -1; | |
int weapon2 = -1; | |
@property ref inout(int[2]) weapons() inout { return *cast(inout(int[2])*)&weapon1; } | |
bool massive; | |
bool flying; | |
PassiveBonus passiveBonus; | |
bool maneuverable; | |
bool volatile; | |
bool hiveLeader; | |
} | |
immutable Unit[] units = | |
[ | |
{ c : 'a', name : "Archive Tank" , party : Party.civilian, maxHP : 1, movement : 4, weapon1 : findWeapon("Stock Cannon") , }, | |
{ c : 'e', name : "Earth Mover" , party : Party.civilian, maxHP : 2, movement : 0 }, | |
{ c : 't', name : "Supply Train" , party : Party.civilian, maxHP : 1, movement : 2, weapon1 : findWeapon("Choo Choo") }, | |
{ c : '1', name : "Combat Mech" , party : Party.player , maxHP : 3 , movement : 3+1, weapon1 : findWeapon("Titan Fist") , massive : true }, | |
{ c : '2', name : "Cannon Mech" , party : Party.player , maxHP : 3 , movement : 3+1, weapon1 : findWeapon("Taurus Cannon") , massive : true, weapon2 : findWeapon("Astra Bombs") }, | |
{ c : '3', name : "Artillery Mech" , party : Party.player , maxHP : 2+2, movement : 3+1, weapon1 : findWeapon("Artemis Artillery") , massive : true, maneuverable : true }, | |
// { c : '?', name : "Unknown" , party : Party.enemy , maxHP :99 }, | |
{ c : 'f', name : "Firefly" , party : Party.enemy , maxHP : 3, movement : 2, weapon1 : findWeapon("Accelerating Thorax"), }, | |
{ c : 'p', name : "Soldier Psion" , party : Party.enemy , maxHP : 2, movement : 2, flying : true, passiveBonus : PassiveBonus.invigoratingSpores }, | |
{ c : 'k', name : "Scorpion" , party : Party.enemy , maxHP : 3, movement : 3, weapon1 : findWeapon("Stinging Spinneret"), }, | |
{ c : 'h', name : "Hornet" , party : Party.enemy , maxHP : 2, movement : 5, weapon1 : findWeapon("Stinger"), flying : true, }, | |
{ c : 'F', name : "Alpha Firefly" , party : Party.enemy , maxHP : 5, movement : 2, weapon1 : findWeapon("Enhanced Thorax"), }, | |
{ c : 'B', name : "Beetle" , party : Party.enemy , maxHP : 4, movement : 2, weapon1 : findWeapon("Pincers"), }, | |
{ c : 'K', name : "Alpha Scorpion" , party : Party.enemy , maxHP : 5, movement : 3, weapon1 : findWeapon("Goring Spinneret"), }, | |
{ c : 'H', name : "Alpha Hornet" , party : Party.enemy , maxHP : 4, movement : 5, flying : true, weapon1 : findWeapon("Launching Stinger"), }, | |
{ c : 'P', name : "Scarab" , party : Party.enemy , maxHP : 2, movement : 3, weapon1 : findWeapon("Spitting Glands") }, | |
{ c : 'P', name : "Alpha Scarab" , party : Party.enemy , maxHP : 4, movement : 3, weapon1 : findWeapon("Spitting Glands 2") }, | |
{ c : 'P', name : "Blast Psion" , party : Party.enemy , maxHP : 2, movement : 2, flying : true, passiveBonus : PassiveBonus.explosiveDecay }, | |
{ c : 'V', name : "Volatile Vek" , party : Party.enemy , maxHP : 4, movement : 3, weapon1 : findWeapon("Stinging Spinneret"), volatile : true }, | |
{ c : 'B', name : "Scorpion Leader" , party : Party.enemy , maxHP : 7, movement : 3, weapon1 : findWeapon("Massive Spinneret"), hiveLeader : true }, | |
]; | |
enum mapSize = 8; | |
struct Coord | |
{ | |
sizediff_t x, y; | |
string toString() const { return format("%d,%d", x, y); } | |
Coord addDir(uint dir) const { Coord result = this; result.x += directions[dir].x; result.y += directions[dir].y; return result; } | |
Coord addDir(uint dir, uint distance) const { Coord result = this; result.x += directions[dir].x * distance; result.y += directions[dir].y * distance; return result; } | |
static Coord fromCell(size_t cell) { return Coord(cell % mapSize, cell / mapSize); } | |
} | |
ref T at(T)(ref T[mapSize][mapSize] grid, Coord coord) { return grid[coord.y][coord.x]; } | |
ref T atCell(T)(ref T[mapSize][mapSize] grid, size_t cell) { return grid[0].ptr[cell]; } | |
struct UnitState { byte unit; Coord pos; byte damageSustained; } | |
enum maxUnits = 16; | |
struct Game | |
{ | |
ubyte[mapSize][mapSize] terrain; | |
ubyte[mapSize][mapSize] environment; | |
byte[mapSize][mapSize] unitMap; | |
UnitState[maxUnits] units; | |
ubyte numUnits; | |
} | |
immutable Coord[] directions = [{0, -1}, {1, 0}, {0, 1}, {-1, 0}]; | |
ubyte reverseDirectior(ubyte dir) { return dir ^ 2; } | |
struct Problem | |
{ | |
Game game; | |
Action[maxUnits] actions; | |
uint numActions; | |
bool[2][maxUnits] haveWeapon; | |
} | |
Problem readProblem(string fileName) | |
{ | |
Problem problem; | |
auto f = File(fileName); | |
foreach (y; 0..mapSize) | |
{ | |
auto l = f.readln().strip; | |
enforce(l.length == mapSize, "Wrong map line size (line %d - %(%s%)) - expected %s, got %s".format(y, [l], mapSize, l.length)); | |
foreach (x; 0..mapSize) | |
{ | |
auto c = l[x]; | |
auto terrain = terrains.countUntil!(t => t.c == c).to!byte; | |
enforce(terrain >= 0, "Unknown terrain char: " ~ c); | |
problem.game.terrain[y][x] = terrain; | |
} | |
} | |
foreach (y; 0..mapSize) | |
problem.game.unitMap[y][] = -1; | |
enforce(f.readln().strip.length == 0, "Blank line after map expected"); | |
while (!f.eof) | |
{ | |
auto l = f.readln().strip; | |
if (!l.length) | |
continue; | |
auto parts = l.split("\t"); | |
if (parts[0] == "E") | |
{ | |
enforce(parts.length == 4, "Wrong number of environment fields"); | |
auto pos = Coord(parts[1].to!ubyte, parts[2].to!ubyte); | |
auto name = parts[3]; | |
auto environment = environments.countUntil!(e => e.name == name).to!byte; | |
enforce(environment >= 0, "Unknown environment name: " ~ name); | |
problem.game.environment.at(pos) = environment; | |
continue; | |
} | |
enforce(parts.length == 5, "Wrong number of fields"); | |
UnitState state; | |
state.pos.x = parts[0].to!ubyte; | |
state.pos.y = parts[1].to!ubyte; | |
state.damageSustained = parts[2].length ? parts[2].to!byte : 0; | |
auto name = parts[4]; | |
auto unit = units.countUntil!(u => u.name == name).to!byte; | |
enforce(unit >= 0, "Unknown unit name: " ~ name); | |
state.unit = unit; | |
if (parts[3].length) | |
{ | |
if (units[unit].party == Party.player) | |
{ | |
foreach (c; parts[3]) | |
problem.haveWeapon[problem.game.numUnits][c - '0'] = true; | |
} | |
else | |
{ | |
auto dir = "NESW".countUntil(parts[3][$-1]).to!byte; | |
enforce(dir >= 0, "Bad attack direction"); | |
auto distance = parts[3].length > 1 ? parts[3][0..$-1].to!ubyte : ubyte(0); | |
problem.actions[problem.numActions++] = Action( | |
problem.game.numUnits, | |
Action.Attack(0, dir, distance)); | |
} | |
} | |
enforce(problem.game.unitMap.at(state.pos) == -1, "Overlapping unit"); | |
problem.game.unitMap.at(state.pos) = problem.game.numUnits; | |
enforce(problem.game.numUnits + 1 < maxUnits, "Too many units"); | |
problem.game.units[problem.game.numUnits++] = state; | |
enforce(!terrains[problem.game.terrain.at(state.pos)].solid, "Unit in solid terrain"); | |
} | |
return problem; | |
} | |
struct Action | |
{ | |
enum Type : ubyte | |
{ | |
none, | |
move, | |
attack, | |
repair, | |
} | |
Type type; | |
byte unit; | |
this(byte unit, Move move) { this.unit = unit; this.type = Type.move; this.move = move; } | |
this(byte unit, Attack attack) { this.unit = unit; this.type = Type.attack; this.attack = attack; } | |
this(byte unit, Repair repair) { this.unit = unit; this.type = Type.repair; this.repair = repair; } | |
union | |
{ | |
struct Move | |
{ | |
Coord pos; | |
} | |
Move move; | |
struct Attack | |
{ | |
ubyte weapon, direction, distance; | |
} | |
Attack attack; | |
struct Repair {} | |
Repair repair; | |
} | |
} | |
enum maxPlayerMechs = 3; | |
// civ. player enemy vol. | |
immutable int[enumLength!Party] damageUnitPoints = [-100, -3, 1, -1]; | |
immutable int[enumLength!Party] destroyUnitPoints = [-500, -1000, 2, -500]; | |
enum enemyEmergePoints = -5; | |
enum damageCivilianBuildingPoints = -10000; | |
enum repairPoints = -damageUnitPoints[Party.player]; | |
enum timePodRecoverPoints = 5; | |
enum timePodDestroyPoints = -10000; | |
Party scoreParty(in ref Unit unit) { return unit.volatile ? Party.volatile : unit.party; } | |
bool inMap(Coord pos) { return(pos.x >= 0 && pos.x < mapSize && pos.y >= 0 && pos.y < mapSize); } | |
alias Score = int; | |
enum badMove = Score.min; | |
struct Simulation(bool verbose, ActionSource) | |
{ | |
Game* game; | |
const Problem* problem; | |
ActionSource* actionSource; | |
this(ref Game game, in ref Problem problem, ref ActionSource actionSource) | |
{ | |
this.game = &game; | |
this.problem = &problem; | |
this.actionSource = &actionSource; | |
} | |
static struct PassiveEffects | |
{ | |
byte hpBoost; | |
bool explosive; | |
} | |
PassiveEffects[maxUnits] passiveEffects; | |
int score = 0; | |
bool bringOutYourDead; | |
void updatePassives() | |
{ | |
passiveEffects[] = PassiveEffects.init; | |
foreach (unit; 0..game.numUnits) | |
if (game.units[unit].unit >= 0) | |
switch (units[game.units[unit].unit].passiveBonus) | |
{ | |
case PassiveBonus.invigoratingSpores: | |
{ | |
auto party = units[game.units[unit].unit].party; | |
foreach (unit2; 0..game.numUnits) | |
if (game.units[unit2].unit >= 0 && units[game.units[unit2].unit].party == party && unit != unit2) | |
passiveEffects[unit2].hpBoost += 1; | |
break; | |
} | |
case PassiveBonus.explosiveDecay: | |
{ | |
auto party = units[game.units[unit].unit].party; | |
foreach (unit2; 0..game.numUnits) | |
if (game.units[unit2].unit >= 0 && units[game.units[unit2].unit].party == party && unit != unit2) | |
passiveEffects[unit2].explosive = true; | |
break; | |
} | |
default: | |
break; | |
} | |
} | |
void clearDeadOnce() | |
{ | |
foreach (ubyte unit; 0..game.numUnits) | |
if (game.units[unit].unit >= 0) | |
{ | |
auto damageSustained = game.units[unit].damageSustained; | |
auto maxHP = units[game.units[unit].unit].maxHP + passiveEffects[unit].hpBoost; | |
if (damageSustained >= maxHP) | |
{ | |
static if (verbose) writefln(" > The %s unit %s at %s destroyed!", | |
units[game.units[unit].unit].party, | |
units[game.units[unit].unit].name, | |
game.units[unit].pos, | |
); | |
destroyUnit(unit); | |
} | |
} | |
} | |
void clearDead() | |
{ | |
if (bringOutYourDead) static if (verbose) writefln(" > Clearing dead units."); | |
while (bringOutYourDead) | |
{ | |
bringOutYourDead = false; | |
clearDeadOnce(); | |
} | |
} | |
void destroyUnit(ubyte unit) | |
{ | |
auto scoreParty = units[game.units[unit].unit].scoreParty; | |
score += destroyUnitPoints[scoreParty]; | |
// Award points for remaining HP, to avoid bias towards damage kills over environment kills | |
auto damageSustained = game.units[unit].damageSustained; | |
auto maxHP = units[game.units[unit].unit].maxHP + passiveEffects[unit].hpBoost; | |
auto remainingHealth = maxHP - damageSustained; | |
if (remainingHealth > 0) | |
{ | |
static if (verbose) writefln(" > Killed %s unit %s still had %d/%d health remaining.", | |
units[game.units[unit].unit].party, | |
units[game.units[unit].unit].name, | |
remainingHealth, maxHP, | |
); | |
score += damageUnitPoints[scoreParty] * remainingHealth; | |
} | |
if (passiveEffects[unit].explosive) | |
{ | |
auto upos = game.units[unit].pos; | |
static if (verbose) writefln(" > Killed %s unit %s activates Explosive Decay!", | |
units[game.units[unit].unit].party, | |
units[game.units[unit].unit].name, | |
remainingHealth, maxHP, | |
); | |
foreach (ubyte dir; 0..4) | |
{ | |
auto tpos = upos.addDir(dir); | |
if (tpos.inMap) | |
{ | |
enum weaponIndex = findWeapon("(explosive decay)"); | |
damageTile(tpos, &weapons[weaponIndex], dir); | |
} | |
} | |
} | |
game.units[unit].unit = -1; | |
game.unitMap.at(game.units[unit].pos) = -1; | |
updatePassives(); | |
bringOutYourDead = true; // may cause a chain reaction | |
} | |
void damageUnit(ubyte unit, byte damage) | |
{ | |
static if (verbose) writefln(" > The %s unit %s at %s is receiving %d points of damage.", | |
units[game.units[unit].unit].party, | |
units[game.units[unit].unit].name, | |
game.units[unit].pos, damage); | |
auto damageSustained = game.units[unit].damageSustained; | |
auto maxHP = units[game.units[unit].unit].maxHP + passiveEffects[unit].hpBoost; | |
static if (verbose) writefln(" > Unit damaged (%d -> %d / %d HP).", | |
maxHP - damageSustained, | |
maxHP - damageSustained - damage, | |
maxHP, | |
); | |
if (damageSustained >= maxHP) | |
{ | |
static if (verbose) writefln(" > Unit is already dead!"); | |
} | |
else | |
if (damageSustained + damage >= maxHP) | |
{ | |
static if (verbose) writefln(" > Unit destroyed!"); | |
score += damageUnitPoints[units[game.units[unit].unit].scoreParty] * (maxHP - damageSustained); | |
} | |
else | |
score += damageUnitPoints[units[game.units[unit].unit].scoreParty] * damage; | |
damageSustained = game.units[unit].damageSustained += damage; | |
if (damageSustained >= maxHP) | |
bringOutYourDead = true; | |
} | |
bool repairUnit(ubyte unit, byte repair) | |
{ | |
static if (verbose) writefln(" > The %s unit %s at %s is repairing for %d points.", | |
units[game.units[unit].unit].party, | |
units[game.units[unit].unit].name, | |
game.units[unit].pos, repair); | |
auto maxHP = units[game.units[unit].unit].maxHP + passiveEffects[unit].hpBoost; | |
auto damageSustained = game.units[unit].damageSustained; | |
if (damageSustained <= repair) | |
repair = damageSustained; | |
if (!repair) | |
{ | |
static if (verbose) writefln(" > Unit is already at full HP!"); | |
return false; | |
} | |
else | |
{ | |
static if (verbose) writefln(" > Unit repaired (%d -> %d / %d HP).", | |
maxHP - damageSustained, | |
maxHP - damageSustained + repair, | |
maxHP, | |
); | |
score += repair * repairPoints; | |
game.units[unit].damageSustained -= repair; | |
return true; | |
} | |
} | |
void checkStop(ubyte unit) | |
{ | |
auto pos = game.units[unit].pos; | |
auto punit = &units[game.units[unit].unit]; | |
switch (game.terrain.at(pos)) | |
{ | |
case TerrainKind.water: | |
if (!punit.massive && !punit.flying && !punit.hiveLeader) | |
{ | |
static if (verbose) writefln(" > Unit is sinking!"); | |
destroyUnit(unit); | |
} | |
break; | |
case TerrainKind.timePod: | |
switch (punit.party) | |
{ | |
case Party.player: | |
static if (verbose) writefln(" > Time pod recovered."); | |
score += timePodRecoverPoints; | |
game.terrain.at(pos) = TerrainKind.ground; | |
break; | |
case Party.enemy: | |
static if (verbose) writefln(" > Time pod trampled!"); | |
score += timePodDestroyPoints; | |
game.terrain.at(pos) = TerrainKind.ground; | |
break; | |
default: | |
break; | |
} | |
break; | |
case TerrainKind.mine: | |
static if (verbose) writefln(" > Unit landed on a land mine!"); | |
destroyUnit(unit); | |
game.terrain.at(pos) = TerrainKind.ground; | |
break; | |
case TerrainKind.chasm: | |
if (!punit.flying) | |
{ | |
static if (verbose) writefln(" > Unit is falling!"); | |
destroyUnit(unit); | |
} | |
break; | |
default: | |
break; | |
} | |
} | |
void moveUnit(ubyte unit, Coord npos) | |
{ | |
auto opos = game.units[unit].pos; | |
assert(opos != npos); | |
assert(game.unitMap.at(npos) == -1); | |
game.unitMap.at(opos) = -1; | |
game.unitMap.at(npos) = unit; | |
game.units[unit].pos = npos; | |
checkStop(unit); | |
} | |
void push(Coord pos, ubyte direction) | |
{ | |
auto tunit = game.unitMap.at(pos); | |
if (tunit >= 0) | |
{ | |
auto ptunit = &units[game.units[tunit].unit]; | |
static if (verbose) writefln(" > %s unit %s at %s is being pushed towards %s.", | |
ptunit.party.text.capitalize, | |
ptunit.name, | |
pos, | |
"NESW"[direction], | |
); | |
auto ppos = pos.addDir(direction); | |
if (!ppos.inMap()) | |
{ | |
static if (verbose) writefln(" > At map boundary, no effect."); | |
return; | |
} | |
auto punit = game.unitMap.at(ppos); | |
if (punit >= 0) | |
{ | |
auto ppunit = &units[game.units[punit].unit]; | |
static if (verbose) writefln(" > Colliding with %s unit %s.", | |
ppunit.party.text.capitalize, | |
ppunit.name, | |
); | |
damageUnit(punit, 1); | |
damageUnit(tunit, 1); | |
return; | |
} | |
auto pterrain = game.terrain.at(ppos); | |
if (terrains[pterrain].solid) | |
{ | |
static if (verbose) writefln(" > Colliding with solid terrain %s.", | |
terrains[pterrain].name, | |
); | |
damageUnit(tunit, 1); | |
damageTerrain(ppos, null); | |
return; | |
} | |
static if (verbose) writefln(" > Unit pushed."); | |
moveUnit(tunit, ppos); | |
} | |
} | |
void damageTerrain(Coord tpos, in Weapon* pweapon) | |
{ | |
switch (game.terrain.at(tpos)) | |
{ | |
case TerrainKind.forest: | |
static if (verbose) writefln(" > Forest catches fire."); | |
game.terrain.at(tpos) = TerrainKind.fire; | |
break; | |
case TerrainKind.mountain: | |
static if (verbose) writefln(" > Damaging mountain."); | |
game.terrain.at(tpos) = TerrainKind.damagedMountain; | |
break; | |
case TerrainKind.damagedMountain: | |
static if (verbose) writefln(" > Breaking mountain."); | |
game.terrain.at(tpos) = TerrainKind.ground; | |
break; | |
case TerrainKind.ice: | |
static if (verbose) writefln(" > Damaging ice."); | |
game.terrain.at(tpos) = TerrainKind.damagedIce; | |
break; | |
case TerrainKind.damagedIce: | |
static if (verbose) writefln(" > Breaking ice."); | |
game.terrain.at(tpos) = TerrainKind.ground; | |
break; | |
case TerrainKind.sand: | |
static if (verbose) writefln(" > Smoking sand."); | |
game.terrain.at(tpos) = TerrainKind.smoke; | |
break; | |
case TerrainKind.civilianBuilding: | |
if (pweapon && pweapon.buildingsImmune) | |
{ | |
static if (verbose) writefln(" > Civilian building immune to weapon."); | |
} | |
else | |
{ | |
static if (verbose) writefln(" > Civilian building damaged!"); | |
score += damageCivilianBuildingPoints; | |
} | |
break; | |
case TerrainKind.timePod: | |
static if (verbose) writefln(" > Time pod destroyed!"); | |
score += timePodDestroyPoints; | |
game.terrain.at(tpos) = TerrainKind.ground; | |
break; | |
case TerrainKind.mine: | |
static if (verbose) writefln(" > Destroying mine."); | |
game.terrain.at(tpos) = TerrainKind.ground; | |
break; | |
default: | |
break; | |
} | |
} | |
void damageTile(Coord tpos, in Weapon* pweapon, ubyte direction) | |
{ | |
auto tunit = game.unitMap.at(tpos); | |
static if (verbose) writefln(" > Target: %s at %s (unit %s).", | |
terrains[game.terrain.at(tpos)].name, | |
tpos, | |
tunit >= 0 ? units[game.units[tunit].unit].name : "(none)", | |
); | |
if (pweapon.damage) | |
{ | |
if (tunit >= 0) | |
damageUnit(tunit, pweapon.damage); | |
damageTerrain(tpos, pweapon); | |
} | |
if (pweapon.push) | |
push(tpos, direction); | |
if (pweapon.pushSurrounding) | |
foreach (ubyte pushDirection; 0..4) | |
{ | |
auto ppos = tpos.addDir(pushDirection); | |
if (ppos.inMap) | |
push(ppos, pushDirection); | |
} | |
} | |
bool isTileSolid(Coord pos) | |
{ | |
return terrains[game.terrain.at(pos)].solid || | |
game.unitMap.at(pos) >= 0; | |
} | |
bool attack(ubyte unit, ubyte weapon, ubyte attackDirection, int attackDistance) | |
{ | |
auto punit = &units[game.units[unit].unit]; | |
auto pweapon = &weapons[punit.weapons[weapon]]; | |
auto upos = game.units[unit].pos; | |
static if (verbose) writefln("> %s unit %d (%s) at %s is firing weapon %d (%s) towards %s%s.", | |
units[game.units[unit].unit].party.text.capitalize, | |
unit, | |
units[game.units[unit].unit].name, | |
upos, | |
weapon, | |
pweapon.name, | |
"NESW"[attackDirection], | |
attackDistance ? format(" (%d tiles away)", attackDistance) : "", | |
); | |
if (pweapon.uses >= 0 /*finite ammo*/ && !problem.haveWeapon[unit][weapon]) | |
{ | |
static if (verbose) writefln(" > Weapon is out of ammo!"); | |
return false; | |
} | |
switch (game.terrain.at(upos)) | |
{ | |
case TerrainKind.water: | |
static if (verbose) writefln(" > Can't attack in water!"); | |
return false; | |
case TerrainKind.smoke: | |
static if (verbose) writefln(" > Can't attack in smoke!"); | |
return false; | |
default: | |
break; | |
} | |
ubyte startDirection, endDirection; | |
if (pweapon.allDirections) | |
{ | |
startDirection = 0; | |
endDirection = 4; | |
} | |
else | |
{ | |
startDirection = endDirection = attackDirection; | |
endDirection++; | |
} | |
foreach (direction; startDirection .. endDirection) | |
{ | |
int startDistance, endDistance; | |
if (pweapon.collide) | |
{ | |
foreach (distance; 0 .. pweapon.maxRange) | |
{ | |
auto tpos = upos.addDir(direction); | |
auto tunit = game.unitMap.at(tpos); | |
if (tunit >= 0) | |
{ | |
static if (verbose) writefln(" > %s unit %s is colliding with %s unit %s at %s!", | |
units[game.units[unit].unit].party.text.capitalize, | |
units[game.units[unit].unit].name, | |
units[game.units[tunit].unit].party, | |
units[game.units[tunit].unit].name, | |
tpos, | |
); | |
damageTile(tpos, pweapon, direction); | |
damageTile(upos, pweapon, direction); | |
break; | |
} | |
moveUnit(unit, tpos); | |
upos = tpos; | |
} | |
return true; | |
} | |
else | |
if (pweapon.piercing) | |
{ | |
startDistance = pweapon.minRange; | |
endDistance = pweapon.maxRange + 1; | |
} | |
else | |
if (pweapon.leap) | |
{ | |
auto tpos = upos.addDir(direction, attackDistance); | |
if (!inMap(tpos)) | |
{ | |
static if (verbose) writefln(" > Leap target at %s is out of bounds.", tpos); | |
return false; | |
} | |
if (isTileSolid(tpos)) | |
{ | |
static if (verbose) writefln(" > Leap target at %s is solid / occupied.", tpos); | |
return false; | |
} | |
static if (verbose) writefln(" > Leaping to attack target %s at %s.", | |
terrains[game.terrain.at(tpos)].name, | |
tpos); | |
moveUnit(unit, tpos); | |
startDistance = 1; | |
endDistance = attackDistance; // attack up to here | |
} | |
else | |
{ | |
int distance; | |
if (pweapon.artillery) | |
distance = attackDistance; | |
else | |
{ | |
distance = pweapon.minRange; | |
while (distance < pweapon.maxRange) | |
{ | |
auto tpos = upos.addDir(direction, distance); | |
if (!tpos.inMap) break; | |
if (isTileSolid(tpos)) break; | |
distance++; | |
} | |
} | |
startDistance = distance; | |
endDistance = distance + 1; | |
} | |
static if (verbose) writefln(" > Attack range: %d .. %d", startDistance, endDistance-1); | |
foreach (distance; startDistance .. endDistance) | |
{ | |
assert(distance > 0); | |
auto tpos = upos.addDir(direction, distance); | |
if (tpos.inMap) | |
damageTile(tpos, pweapon, direction); | |
else | |
{ | |
static if (verbose) writefln(" > Target at %s is out of bounds.", tpos); | |
if (!pweapon.charge) | |
return false; | |
} | |
if (pweapon.charge) | |
{ | |
if (distance > 1) | |
{ | |
auto npos = upos; | |
npos.x += directions[direction].x * (distance-1); | |
npos.y += directions[direction].y * (distance-1); | |
assert(npos.inMap); | |
assert(!terrains[game.terrain.at(npos)].solid); | |
static if (verbose) writefln(" > Charge attack moved attacking unit to %s.", npos); | |
moveUnit(unit, npos); | |
} | |
} | |
} | |
} | |
return true; | |
} | |
const(ubyte)[] attackDirections(in ref Weapon weapon, ubyte direction) | |
{ | |
static immutable ubyte[] dirs = [0, 1, 2, 3]; | |
if (weapon.allDirections) | |
return dirs[]; | |
else | |
return dirs[direction .. direction+1]; | |
} | |
bool canMove(ubyte unit) | |
{ | |
foreach (ref action; problem.actions[0..problem.numActions]) | |
if (action.type == Action.Type.attack && | |
game.units[action.unit].unit >= 0) | |
{ | |
auto pweapon = &weapons[units[game.units[action.unit].unit].weapons[action.attack.weapon]]; | |
if (pweapon.webbing) | |
foreach (direction; attackDirections(*pweapon, action.attack.direction)) | |
if (game.units[action.unit].pos.addDir(direction) == game.units[unit].pos) | |
{ | |
static if (verbose) writefln(" > Unit can't move because it is webbed by %s unit %s at %s.", | |
units[game.units[action.unit].unit].party, | |
units[game.units[action.unit].unit].name, | |
game.units[action.unit].pos, | |
); | |
return false; | |
} | |
} | |
return true; | |
} | |
bool performAction(in ref Action action) | |
{ | |
if (!action.type) | |
{ | |
static if (verbose) writeln("> No action"); | |
return true; | |
} | |
if (game.units[action.unit].unit < 0) | |
{ | |
static if (verbose) writefln("> Unit %d is already dead.", action.unit); | |
return true; | |
} | |
final switch (action.type) | |
{ | |
case Action.Type.none: | |
assert(false); | |
case Action.Type.move: | |
auto opos = game.units[action.unit].pos; | |
auto npos = action.move.pos; | |
assert(units[game.units[action.unit].unit].party == Party.player); | |
if (opos == npos) | |
{ | |
static if (verbose) writefln("> %s unit %d (%s) is standing in place at %s on %s.", | |
units[game.units[action.unit].unit].party.text.capitalize, | |
action.unit, | |
units[game.units[action.unit].unit].name, | |
opos, | |
terrains[game.terrain.at(opos)].name, | |
); | |
return true; | |
} | |
static if (verbose) writefln("> %s unit %d (%s) at %s is moving to %s (%d%s %d%s) from %s to %s.", | |
units[game.units[action.unit].unit].party.text.capitalize, | |
action.unit, | |
units[game.units[action.unit].unit].name, | |
opos, | |
npos, | |
abs(npos.y - opos.y), "NS"[npos.y > opos.y], | |
abs(npos.x - opos.x), "WE"[npos.x > opos.x], | |
terrains[game.terrain.at(opos)].name, | |
terrains[game.terrain.at(npos)].name, | |
); | |
if (!canMove(action.unit)) | |
return false; // shouldn't even try | |
game.unitMap.at(opos) = -1; | |
if (game.unitMap.at(npos) >= 0) | |
{ | |
static if (verbose) writefln(" > Move failed (unit collision)."); | |
game.unitMap.at(opos) = action.unit; | |
return false; | |
} | |
game.unitMap.at(npos) = action.unit; | |
game.units[action.unit].pos = npos; | |
checkStop(action.unit); | |
break; | |
case Action.Type.attack: | |
if (!attack(action.unit, action.attack.weapon, action.attack.direction, action.attack.distance)) | |
return false; | |
break; | |
case Action.Type.repair: | |
static if (verbose) writefln("> %s unit %d (%s) is repairing.", | |
units[game.units[action.unit].unit].party.text.capitalize, | |
action.unit, | |
units[game.units[action.unit].unit].name, | |
); | |
if (game.terrain.at(game.units[action.unit].pos) == TerrainKind.smoke) | |
{ | |
static if (verbose) writefln(" > Can't repair in smoke!"); | |
return false; | |
} | |
if (!repairUnit(action.unit, 1)) | |
return false; | |
break; | |
} | |
clearDead(); | |
return true; | |
} | |
Score play() | |
{ | |
updatePassives(); | |
static if (verbose) writeln("=== Player turn ==="); | |
foreach (n; 0 .. 2 * maxPlayerMechs) | |
{ | |
auto action = actionSource.getAction(*game); | |
if (!performAction(action)) | |
return badMove; | |
} | |
static if (verbose) writeln("=== Terrain effects 1 ==="); | |
foreach (cell; 0 .. mapSize * mapSize) | |
switch (game.terrain.atCell(cell)) | |
{ | |
case TerrainKind.fire: | |
if (game.unitMap.atCell(cell) >= 0) | |
{ | |
static if (verbose) writefln("> Fire at %s is burning %s unit %s.", | |
Coord.fromCell(cell), | |
units[game.units[game.unitMap.atCell(cell)].unit].party, | |
units[game.units[game.unitMap.atCell(cell)].unit].name, | |
); | |
damageUnit(game.unitMap.atCell(cell), 1); | |
} | |
break; | |
default: | |
break; | |
} | |
clearDead(); | |
static if (verbose) writeln("=== Environments ==="); | |
foreach (cell; 0 .. mapSize * mapSize) | |
switch (game.environment.atCell(cell)) | |
{ | |
case EnvironmentKind.airSupport: | |
if (game.unitMap.atCell(cell) >= 0) | |
{ | |
static if (verbose) writefln("> Air support at %s is obliterating %s unit %s.", | |
Coord.fromCell(cell), | |
units[game.units[game.unitMap.atCell(cell)].unit].party, | |
units[game.units[game.unitMap.atCell(cell)].unit].name, | |
); | |
destroyUnit(game.unitMap.atCell(cell)); | |
} | |
break; | |
default: | |
break; | |
} | |
static if (verbose) writeln("=== Enemy turn ==="); | |
foreach (action; problem.actions[0..problem.numActions]) | |
performAction(action); | |
static if (verbose) writeln("=== Terrain effects 2 ==="); | |
foreach (cell; 0 .. mapSize * mapSize) | |
switch (game.terrain.atCell(cell)) | |
{ | |
case TerrainKind.emerging: | |
static if (verbose) writefln("> Enemy attempting to emerge at %s.", Coord.fromCell(cell)); | |
if (game.unitMap.atCell(cell) >= 0) | |
{ | |
static if (verbose) writefln(" > Emerging enemy blocked by %s unit %s.", | |
units[game.units[game.unitMap.atCell(cell)].unit].party, | |
units[game.units[game.unitMap.atCell(cell)].unit].name, | |
); | |
damageUnit(game.unitMap.atCell(cell), 1); | |
} | |
else | |
{ | |
static if (verbose) writefln(" > Enemy emerges successfully."); | |
score += enemyEmergePoints; | |
} | |
break; | |
default: | |
break; | |
} | |
clearDead(); | |
return score; | |
} | |
} | |
struct RandomActionSource(NumberSource) | |
{ | |
ubyte[maxPlayerMechs] mechUnits; | |
ubyte[maxPlayerMechs] mechStatus; | |
NumberSource numberSource; | |
this(in ref Problem problem, NumberSource numberSource) | |
{ | |
this.numberSource = numberSource; | |
ubyte mechUnitCounter; | |
mechStatus[] = 0xFF; | |
foreach (ubyte unit; 0..problem.game.numUnits) | |
if (units[problem.game.units[unit].unit].party == Party.player) | |
{ | |
mechUnits[mechUnitCounter] = unit; | |
mechStatus[mechUnitCounter++] = 0; // ready to go! | |
} | |
} | |
debug(CheckAction) | |
{ | |
int currentAction; | |
bool check(Action action) | |
{ | |
static immutable correctActions = [ | |
Action(1, Action.Move(4, 3)), | |
Action(2, Action.Move(1, 3)), | |
Action(2, Action.Attack(0, 1, 2)), | |
Action(1, Action.Attack(0, 2, 3)), | |
Action(0, Action.Move(5, 1)), | |
Action(0, Action.Attack(0, 2, 1)), | |
]; | |
if (action == correctActions[currentAction]) | |
{ | |
writeln("Found correct action # ", currentAction); | |
currentAction++; | |
return true; | |
} | |
else | |
return false; | |
} | |
} | |
Action getAction(in ref Game game) | |
{ | |
ubyte[maxPlayerMechs] availableMechs = void; | |
ubyte numAvailableMechs; | |
foreach (ubyte mech; 0..maxPlayerMechs) | |
if (mechStatus[mech] < 2 && // can do something | |
game.units[mechUnits[mech]].unit >= 0) // is alive | |
availableMechs[numAvailableMechs++] = mech; | |
if (numAvailableMechs == 0) | |
return Action.init; | |
debug(CheckAction) { retry: } | |
auto mech = availableMechs[numberSource.getNumber(numAvailableMechs)]; | |
auto punit = &units[game.units[mechUnits[mech]].unit]; | |
Coord pos = game.units[mechUnits[mech]].pos; | |
switch (mechStatus[mech]) | |
{ | |
case 0: // time to move | |
{ | |
Coord[mapSize * mapSize] queue = void; | |
bool[mapSize][mapSize] visited; | |
ubyte queueHead, queueTail; | |
void enqueue(Coord pos) | |
{ | |
auto pvisited = &visited.at(pos); | |
assert(!*pvisited); | |
*pvisited = true; | |
queue[queueHead++] = pos; | |
} | |
enqueue(pos); | |
foreach (step; 0..punit.movement) | |
{ | |
auto stepEnd = queueHead; | |
while (queueTail < stepEnd) | |
{ | |
auto qpos = queue[queueTail++]; | |
foreach (dir; 0..4) | |
{ | |
auto tpos = qpos.addDir(dir); | |
if (tpos.inMap && | |
!visited.at(tpos) && | |
!terrains[game.terrain.at(tpos)].solid && | |
(game.unitMap.at(tpos) < 0 || units[game.units[game.unitMap.at(tpos)].unit].party != Party.enemy || punit.maneuverable)) | |
enqueue(tpos); | |
} | |
} | |
} | |
auto npos = queue[numberSource.getNumber(queueHead)]; | |
debug(CheckAction) if (!check(Action(mechUnits[mech], Action.Move(cast(byte)nx, cast(byte)ny)))) | |
goto retry; | |
mechStatus[mech]++; | |
return Action(mechUnits[mech], Action.Move(npos)); | |
} | |
case 1: // time to act | |
switch (numberSource.getNumber(3)) | |
{ | |
case 0: // none | |
debug(CheckAction) if (!check(Action.init)) | |
goto retry; | |
mechStatus[mech]++; | |
return Action.init; | |
case 1: // attack | |
{ | |
Action.Attack[typeof(punit.weapons).length * directions.length * (mapSize - 1)] attacks = void; | |
uint numAttacks; | |
ubyte numWeapons = punit.weapon2 >= 0 ? 2 : 1; | |
foreach (ubyte weapon; 0..numWeapons) | |
{ | |
auto pweapon = &weapons[punit.weapons[weapon]]; | |
byte minRange, maxRange; | |
if (pweapon.artillery) | |
{ | |
minRange = cast(byte)pweapon.minRange; | |
maxRange = cast(byte)pweapon.maxRange; | |
} | |
maxRange++; // up to | |
foreach (ubyte direction; 0..4) | |
foreach (ubyte distance; minRange .. maxRange) | |
{ | |
auto tpos = pos.addDir(direction); | |
if (tpos.inMap) | |
attacks[numAttacks++] = Action.Attack(weapon, direction, distance); | |
} | |
} | |
assert(numAttacks); | |
auto attack = attacks[numberSource.getNumber(numAttacks)]; | |
debug(CheckAction) if (!check(Action(mechUnits[mech], attack))) | |
goto retry; | |
mechStatus[mech]++; | |
return Action(mechUnits[mech], attack); | |
} | |
case 2: // repair | |
debug(CheckAction) if (!check(Action(mechUnits[mech], Action.Repair()))) | |
goto retry; | |
mechStatus[mech]++; | |
return Action(mechUnits[mech], Action.Repair()); | |
default: | |
assert(false); | |
} | |
case 2: // already went? | |
assert(false); | |
default: | |
assert(false); | |
} | |
} | |
} | |
struct ManualActionSource | |
{ | |
Action[] actions; | |
Action getAction(in ref Game game) | |
{ | |
if (actions.length) | |
{ | |
auto action = actions[0]; | |
actions = actions[1..$]; | |
return action; | |
} | |
return Action.init; | |
} | |
} | |
struct RandomNumberSource | |
{ | |
std.random.Mt19937_64 rng; | |
struct State | |
{ | |
ulong seed; | |
this(uint sliceNumber, uint totalSlices) | |
{ | |
} | |
bool advance() | |
{ | |
//seed++; | |
seed = std.random.uniform!ulong(); | |
return false; | |
} | |
} | |
this(State* state) | |
{ | |
rng.seed(state.seed); | |
} | |
uint getNumber(uint max) | |
{ | |
return std.random.uniform(0, max, rng); | |
} | |
} | |
enum maxNumberChoices = maxPlayerMechs * 5; // order (2x); move position; action kind; action (attack choice) | |
uint numBits(T)(T v) { return v ? 1 + bsr(v) : 0; } | |
mixin DeclareException!q{SliceSlackException}; | |
__gshared bool printProgress = true; | |
struct SerialNumberSource | |
{ | |
struct State | |
{ | |
uint[maxNumberChoices] cur, max; | |
uint depth; | |
uint depthBits; | |
uint sliceBits; | |
uint sliceStart, sliceEnd; // in range 0 .. 1<<sliceBits | |
this(uint sliceNumber, uint totalSlices) | |
{ | |
assert((totalSlices & (totalSlices-1)) == 0, "Number of slices is not a power of two"); | |
sliceBits = numBits(totalSlices-1); | |
sliceStart = sliceNumber; | |
sliceEnd = sliceNumber + 1; | |
} | |
// Return position as numerator with denominator 1<<sliceBits. | |
uint getSlicePos() | |
{ | |
uint slicePos; | |
uint slicePosBits; | |
uint pos; | |
while (slicePosBits < sliceBits) | |
{ | |
if (pos == depth) | |
return slicePos << (sliceBits - slicePosBits); // rest is zero | |
auto bits = numBits(max[pos]-1); | |
slicePos = (slicePos << bits) | cur[pos]; | |
pos++; | |
slicePosBits += bits; | |
} | |
// Shave off extra bits | |
return slicePos >> (slicePosBits - sliceBits); | |
} | |
bool advance() | |
{ | |
while (depth) | |
{ | |
cur[depth-1]++; | |
if (cur[depth-1] == max[depth-1]) | |
{ | |
debug(slice) writeln(" < depth:", depth, "->", depth-1, " depthBits:", depthBits, "->", depthBits - numBits(max[depth-1]-1)); | |
depth--; | |
depthBits -= numBits(max[depth]-1); | |
} | |
else | |
break; | |
} | |
if (depth < 5) | |
{ | |
if (printProgress) | |
{ | |
stderr.writef("[%2d/%d]", sliceStart, 1<<sliceBits); | |
foreach (n; 0..depth) | |
stderr.writef(" %d/%d", cur[n], max[n]); | |
stderr.writeln(); | |
} | |
// Depth can be 0 in two cases: | |
// - Initial state (representing 0.0) | |
// - Final state (representing 1.0). | |
// Here, it can only be the final state. | |
if (depth == 0) | |
return false; | |
debug(slice) writeln(" + depth=", depth, " depthBits=", depthBits, " sliceBits=", sliceBits, " slicePos=", getSlicePos); | |
if (depthBits - numBits(max[depth-1]-1) <= sliceBits) | |
if (getSlicePos() >= sliceEnd) | |
return false; | |
} | |
return true; | |
} | |
} | |
State* state; | |
uint pos; | |
uint getNumber(uint max) | |
{ | |
assert(max > 0); | |
if (pos < state.depth) | |
{ | |
assert(max == state.max[pos], "Unexpected upper bound change"); | |
return state.cur[pos++]; | |
} | |
else | |
{ | |
//debug scope(failure) writeln([pos, state.depth, max, maxNumberChoices]); | |
assert(pos == state.depth); | |
uint bits = numBits(max-1); | |
auto endBits = state.depthBits + bits; | |
uint cur; | |
debug(slice) writeln(" > depth:", state.depth, "->", state.depth+1, " depthBits:", state.depthBits, "->", endBits, " max=", max); | |
if (state.depthBits < state.sliceBits && state.getSlicePos() <= state.sliceStart) | |
{ | |
if (state.sliceBits < endBits) | |
cur = state.sliceStart << (endBits - state.sliceBits); | |
else | |
cur = state.sliceStart >> (state.sliceBits - endBits); | |
cur &= (1<<bits)-1; | |
if (cur >= max) | |
throw new SliceSlackException("Slice slack"); | |
} | |
else | |
cur = 0; | |
state.cur[pos] = cur; | |
state.max[pos] = max; | |
state.depth++; | |
state.depthBits = endBits; | |
pos++; | |
return cur; | |
} | |
} | |
} | |
unittest | |
{ | |
uint getMax(uint[] choices) | |
{ | |
if (choices == []) | |
return 1; | |
if (choices == [0]) | |
return 6; | |
if (choices == [0, 2]) | |
return 3; | |
if (choices == [0, 2, 2]) | |
return 1; | |
if (choices == [0, 4]) | |
return 1; | |
if (choices == [0, 4, 0]) | |
return 1; | |
if (choices == [0, 4, 0, 0]) | |
return 1; | |
return 0; // no more | |
} | |
foreach (sliceBits; 0 .. 4) | |
{ | |
uint[][] allChoices; | |
foreach (slice; 0 .. 1 << sliceBits) | |
{ | |
auto state = SerialNumberSource.State(slice, 1 << sliceBits); | |
do | |
{ | |
try | |
{ | |
uint[] choices; | |
auto source = SerialNumberSource(&state); | |
while (true) | |
{ | |
auto nextMax = getMax(choices); | |
if (!nextMax) | |
break; | |
auto nextChoice = source.getNumber(nextMax); | |
choices ~= nextChoice; | |
} | |
allChoices ~= choices; | |
} | |
catch (SliceSlackException) {} | |
} while (state.advance()); | |
} | |
assert(allChoices == [ | |
[0, 0], | |
[0, 1], | |
[0, 2, 0], | |
[0, 2, 1], | |
[0, 2, 2, 0], | |
[0, 3], | |
[0, 4, 0, 0, 0], | |
[0, 5], | |
]); | |
} | |
} | |
Score solveProblem(in ref Problem problem) | |
{ | |
version (none) | |
{{ | |
ManualActionSource solution; | |
int actions; | |
// solution.actions ~= Action(2, Action.Attack(0, 1, 2)); | |
// solution.actions ~= Action(0, Action.Move(Coord(5, 1))); | |
// solution.actions ~= Action(1, Action.Move(Coord(4, 3))); | |
// solution.actions ~= Action(2, Action.Move(Coord(1, 3))); | |
// solution.actions ~= Action(1, Action.Attack(1, 2, 5)); | |
// solution.actions ~= Action(0, Action.Attack(0, 2)); | |
Game game = problem.game; | |
auto vsimulation = Simulation!(true, ManualActionSource)(game, problem, solution); | |
auto score = vsimulation.play(); | |
writeln("Score: ", score); | |
return score; | |
}} | |
//alias NumberSource = RandomNumberSource; | |
alias NumberSource = SerialNumberSource; | |
alias ActionSource = RandomActionSource!NumberSource; | |
Score bestScore = Score.min; | |
NumberSource.State bestSolution; | |
auto numSlices = 1 << (numBits(totalCPUs - 1) + 1); | |
foreach (slice; numSlices.iota.parallel) | |
{ | |
auto numberSourceState = NumberSource.State(slice, numSlices); | |
auto localBestScore = int.min; | |
do | |
{ | |
auto numberSource = NumberSource(&numberSourceState); | |
debug scope(failure) | |
{ | |
writeln("Error trace:"); | |
Game game = problem.game; | |
auto actionSource = ActionSource(problem, numberSource); | |
auto vsimulation = Simulation!(true, ActionSource)(game, problem, actionSource); | |
vsimulation.play(); | |
} | |
Game game = problem.game; | |
auto actionSource = ActionSource(problem, numberSource); | |
auto simulation = Simulation!(false, ActionSource)(game, problem, actionSource); | |
Score score = Score.min; | |
try | |
score = simulation.play(); | |
catch (SliceSlackException) {} | |
if (score > localBestScore) | |
{ | |
localBestScore = score; | |
synchronized if (score > bestScore) | |
{ | |
writeln(); | |
writeln(); | |
writeln(); | |
writeln("New best score: ", score); | |
writeln(); | |
game = problem.game; | |
actionSource = ActionSource(problem, numberSource); | |
auto vsimulation = Simulation!(true, ActionSource)(game, problem, actionSource); | |
vsimulation.play(); | |
writeln(); | |
writeln(); | |
bestScore = score; | |
bestSolution = numberSourceState; | |
} | |
} | |
} | |
while (numberSourceState.advance()); | |
} | |
writeln(); | |
writeln("Best score: ", bestScore); | |
writeln(); | |
Game game = problem.game; | |
auto numberSource = NumberSource(&bestSolution); | |
auto actionSource = ActionSource(problem, numberSource); | |
auto vsimulation = Simulation!(true, ActionSource)(game, problem, actionSource); | |
vsimulation.play(); | |
return bestScore; | |
} | |
Problem generateProblem(byte[] playerUnits) | |
{ | |
Problem problem; | |
Coord[] placementChoices; | |
// Generate terrain | |
{ | |
TerrainKind[] choices; | |
bool[enumLength!TerrainKind] placementOK; | |
foreach (TerrainKind terrain; TerrainKind.init .. enumLength!TerrainKind) | |
{ | |
uint share = 10; | |
switch (terrain) | |
{ | |
case TerrainKind.ground: | |
share = 50; | |
placementOK[terrain] = true; | |
break; | |
case TerrainKind.emerging: | |
share = 5; | |
break; | |
case TerrainKind.timePod: | |
share = 1; | |
break; | |
case TerrainKind.forest: | |
case TerrainKind.ice: | |
case TerrainKind.sand: | |
placementOK[terrain] = true; | |
break; | |
default: | |
break; | |
} | |
foreach (n; 0..share) | |
choices ~= terrain; | |
} | |
foreach (y; 0 .. mapSize) | |
foreach (x; 0 .. mapSize) | |
{ | |
auto terrain = choices[std.random.uniform(0, $)]; | |
problem.game.terrain[y][x] = cast(ubyte)terrain; | |
if (placementOK[terrain]) | |
placementChoices ~= Coord(x, y); | |
} | |
} | |
byte[] unitsToPlace = playerUnits; | |
auto viableUnits = (cast(byte)units.length).iota.filter!(unit => units[unit].party != Party.player).array; | |
foreach (n; 0 .. std.random.uniform(1, 5)) | |
unitsToPlace ~= viableUnits[std.random.uniform(0, $)]; | |
foreach (y; 0..mapSize) | |
problem.game.unitMap[y][] = -1; | |
foreach (unit; unitsToPlace) | |
{ | |
if (!placementChoices.length) | |
break; | |
import ae.utils.array : pluck; | |
auto pos = placementChoices.pluck(); | |
problem.game.units[problem.game.numUnits] = UnitState(unit, pos, cast(byte)(std.random.uniform(0, 1000) * units[unit].maxHP / 1000)); | |
problem.game.unitMap.at(pos) = problem.game.numUnits; | |
if (units[unit].party == Party.enemy && units[unit].weapon1 >= 0) | |
problem.actions[problem.numActions++] = Action( | |
problem.game.numUnits, | |
Action.Attack( | |
0, | |
std.random.uniform(ubyte(0), ubyte(4)), | |
std.random.uniform(ubyte(1), ubyte(8)), | |
)); | |
foreach (weapon; 0..2) | |
{ | |
enum numTurnsPerEncounter = 5; // approx. | |
auto weaponChance = std.random.uniform(0, numTurnsPerEncounter); | |
if (units[unit].weapons[weapon] >= 0 && weaponChance < weapons[units[unit].weapons[weapon]].uses) | |
problem.haveWeapon[problem.game.numUnits][weapon] = true; | |
} | |
problem.game.numUnits++; | |
} | |
return problem; | |
} | |
void printProblem(in ref Problem problem) | |
{ | |
writeln(); | |
writeln("Terrain:"); | |
writeln(); | |
foreach (y; 0..mapSize) | |
{ | |
foreach (x; 0..mapSize) | |
write(terrains[problem.game.terrain[y][x]].c); | |
writeln(); | |
} | |
writeln(); | |
writeln("Environment:"); | |
writeln(); | |
foreach (y; 0..mapSize) | |
{ | |
foreach (x; 0..mapSize) | |
write(environments[problem.game.environment[y][x]].c); | |
writeln(); | |
} | |
writeln(); | |
writeln("Units:"); | |
writeln(); | |
foreach (y; 0..mapSize) | |
{ | |
foreach (x; 0..mapSize) | |
write(problem.game.unitMap[y][x] >= 0 ? units[problem.game.units[problem.game.unitMap[y][x]].unit].c : ' '); | |
writeln(); | |
} | |
} | |
version (unittest_only) {} else | |
void main(string[] args) | |
{ | |
// rndGen.seed(0); | |
if (args.length == 1) | |
{ | |
auto problem = readProblem("problem.txt"); | |
solveProblem(problem); | |
} | |
else | |
if (args[1] == "--loadout") | |
{ | |
printProgress = false; | |
auto playerUnits = args[2..$].map!(arg => cast(byte)units.countUntil!(unit => unit.name == arg)).array; | |
enum numProblems = 1000; | |
Score totalScore; | |
foreach (n; numProblems.iota/*.parallel*/) | |
{ | |
std.random.rndGen.seed(n); | |
auto problem = generateProblem(playerUnits); | |
printProblem(problem); | |
auto score = solveProblem(problem); | |
synchronized totalScore += score; | |
} | |
writeln(); | |
writeln("================================="); | |
writeln("Final score total: ", totalScore); | |
} | |
else | |
{ | |
auto files = args[1..$]; | |
auto scores = new Score[files.length]; | |
foreach (i, fn; files.parallel) | |
{ | |
auto problem = readProblem(fn); | |
scores[i] = solveProblem(problem); | |
} | |
writeln(); | |
writeln("================================="); | |
foreach (i, fn; files) | |
writefln("%s: %d", fn, scores[i]); | |
writeln("Final score total: ", scores.sum); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment