Skip to content

Instantly share code, notes, and snippets.

@CyberShadow
Last active April 4, 2023 13:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save CyberShadow/156f1582b2fc8108e74b5e12965fccc0 to your computer and use it in GitHub Desktop.
Save CyberShadow/156f1582b2fc8108e74b5e12965fccc0 to your computer and use it in GitHub Desktop.
Into The Breach solver
/problem.txt
/solver
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