diff --git a/README.md b/README.md index 893376f..2c057a6 100644 --- a/README.md +++ b/README.md @@ -35,15 +35,6 @@ image to appear. The parameters are: * iterations, playouts: configure AI parameters - -# URGENT - -Recently I tried to fix a bug: if a unit ignores flags and stays in its hex, it should battle back. - - - now the AI is broken - - it is possible to keep ignoring flags and battling back multiple times! - - change Phase#onClick so that it returns a command to execute -- this will make it easier to test - # AI TESTS - Early game @@ -72,12 +63,13 @@ Recently I tried to fix a bug: if a unit ignores flags and stays in its hex, it - evasion - more cards -- advance after combat -- cavalry, chariot battle again after advance +- cavalry bonus movement +- cavalry, chariot bonus combat - leaders - terrain - "if you don't have any X unit, order a unit of your choice" - heavy chariot battles back with 3 dice not 4 - heavy chariot ignores one sword result +- warriors, elephants, other units diff --git a/src/ai/__snapshots__/mcts_player.test.js.snap b/src/ai/__snapshots__/mcts_player.test.js.snap index 24ac376..92d4505 100644 --- a/src/ai/__snapshots__/mcts_player.test.js.snap +++ b/src/ai/__snapshots__/mcts_player.test.js.snap @@ -6,13 +6,13 @@ exports[`2 on 2 1`] = ` 333/1999: End phase -> Roman movement -> 1 333/1998: MacroCommand(Move [-1,6] to [0,5],Move [0,6] to [1,5]) -> Roman battle -> 3 CHANCE/49: Close Combat from [1,5] to [1,4] -> Roman battle -> 6 - -8/8: Close Combat from [1,5] to [1,4] -> Roman retreat -> 2 + -8/8: Close Combat from [1,5] to [1,4] -> Roman attacker retreat -> 2 -4/4: Retreat [1,5] to [0,6] -> Roman battle -> 2 -4/4: Retreat [1,5] to [1,6] -> Roman battle -> 2 -17/20: Close Combat from [1,5] to [1,4] -> Carthaginian retreat -> 2 0/0: Retreat [1,4] to [2,3] -> Roman advance after combat -> 0 17/20: Retreat [1,4] to [1,3] -> Roman advance after combat -> 2 - -4/7: Close Combat from [1,5] to [1,4] -> Roman retreat -> 2 + -4/7: Close Combat from [1,5] to [1,4] -> Roman attacker retreat -> 2 -2/5: Retreat [1,5] to [0,6] -> Roman battle -> 2 -2/2: Retreat [1,5] to [1,6] -> Roman battle -> 2 -4/4: Close Combat from [1,5] to [1,4] -> Carthaginian retreat -> 2 @@ -21,18 +21,18 @@ exports[`2 on 2 1`] = ` -2/2: Close Combat from [1,5] to [1,4] -> Carthaginian retreat -> 2 2/2: Retreat [1,4] to [2,3] -> Roman advance after combat -> 2 0/0: Retreat [1,4] to [1,3] -> Roman advance after combat -> 0 - 2/7: Close Combat from [1,5] to [1,4] -> Roman retreat -> 2 + 2/7: Close Combat from [1,5] to [1,4] -> Roman attacker retreat -> 2 2/4: Retreat [1,5] to [0,6] -> Roman battle -> 2 0/3: Retreat [1,5] to [1,6] -> Roman battle -> 2 CHANCE/740: Close Combat from [0,5] to [0,4] -> Roman battle -> 2 -137/472: Close Combat from [0,5] to [0,4] -> Carthaginian retreat -> 2 0/0: Retreat [0,4] to [0,3] -> Roman advance after combat -> 0 137/472: Retreat [0,4] to [1,3] -> Roman advance after combat -> 2 - 84/267: Close Combat from [0,5] to [0,4] -> Roman retreat -> 2 + 84/267: Close Combat from [0,5] to [0,4] -> Roman attacker retreat -> 2 81/209: Retreat [0,5] to [-1,6] -> Roman battle -> 1 3/58: Retreat [0,5] to [0,6] -> Roman battle -> 1 CHANCE/1210: Close Combat from [0,5] to [1,4] -> Roman battle -> 5 - 126/347: Close Combat from [0,5] to [1,4] -> Roman retreat -> 2 + 126/347: Close Combat from [0,5] to [1,4] -> Roman attacker retreat -> 2 131/298: Retreat [0,5] to [-1,6] -> Roman battle -> 1 -5/49: Retreat [0,5] to [0,6] -> Roman battle -> 1 108/511: Close Combat from [0,5] to [1,4] -> Carthaginian retreat -> 2 @@ -41,7 +41,7 @@ exports[`2 on 2 1`] = ` -70/133: Close Combat from [0,5] to [1,4] -> Carthaginian retreat -> 2 70/133: Retreat [1,4] to [2,3] -> Roman advance after combat -> 2 0/0: Retreat [1,4] to [1,3] -> Roman advance after combat -> 0 - -66/110: Close Combat from [0,5] to [1,4] -> Roman retreat -> 2 + -66/110: Close Combat from [0,5] to [1,4] -> Roman attacker retreat -> 2 -32/52: Retreat [0,5] to [-1,6] -> Roman battle -> 1 -34/58: Retreat [0,5] to [0,6] -> Roman battle -> 1 -76/108: Close Combat from [0,5] to [1,4] -> Carthaginian retreat -> 2 @@ -53,22 +53,22 @@ exports[`2 on 2 1`] = ` exports[`close combat from light to heavy 1`] = ` "-24/30: undefined -> Roman play one card -> 1 -24/30: PlayCard(Order Light Troops) -> Roman order 1 light units -> 1 - -23/29: End phase -> Roman movement -> 1 + -24/29: End phase -> Roman movement -> 1 -23/28: MacroCommand(Move [2,3] to [3,2]) -> Roman battle -> 1 CHANCE/27: Close Combat from [3,2] to [2,2] -> Roman battle -> 5 -1/4: Close Combat from [3,2] to [2,2] -> Carthaginian retreat -> 2 1/4: Retreat [2,2] to [2,1] -> Roman advance after combat -> 2 - 0/1: Retreat [3,2] to [2,2] -> Roman battle -> 0 - 2/2: Retreat [3,2] to [3,2] -> Roman battle -> 1 + 0/1: Momentum advance [3,2] to [2,2] -> Roman battle -> 0 + 2/2: Skip Momentum Advance -> Roman battle -> 1 0/0: Retreat [2,2] to [3,1] -> Roman advance after combat -> 0 - -8/8: Close Combat from [3,2] to [2,2] -> Roman retreat -> 3 + -8/8: Close Combat from [3,2] to [2,2] -> Roman attacker retreat -> 3 -3/3: Retreat [3,2] to [2,4] -> Roman battle -> 1 2/2: End phase -> Carthaginian play one card -> 1 -2/2: Retreat [3,2] to [3,4] -> Roman battle -> 1 2/2: End phase -> Carthaginian play one card -> 1 -3/3: Retreat [3,2] to [1,4] -> Roman battle -> 1 3/3: End phase -> Carthaginian play one card -> 1 - -4/4: Close Combat from [3,2] to [2,2] -> Roman retreat -> 3 + -4/4: Close Combat from [3,2] to [2,2] -> Roman attacker retreat -> 3 -2/2: Retreat [3,2] to [1,4] -> Roman battle -> 1 1/1: End phase -> Carthaginian play one card -> 0 -1/1: Retreat [3,2] to [3,4] -> Roman battle -> 1 @@ -77,11 +77,108 @@ exports[`close combat from light to heavy 1`] = ` 1/1: End phase -> Carthaginian play one card -> 0 3/3: Close Combat from [3,2] to [2,2] -> Carthaginian retreat -> 2 -3/3: Retreat [2,2] to [3,1] -> Roman advance after combat -> 2 - -1/1: Retreat [3,2] to [2,2] -> Roman battle -> 0 - -1/1: Retreat [3,2] to [3,2] -> Roman battle -> 1 + -1/1: Momentum advance [3,2] to [2,2] -> Roman battle -> 0 + -1/1: Skip Momentum Advance -> Roman battle -> 1 0/0: Retreat [2,2] to [2,1] -> Roman advance after combat -> 0 -7/7: Close Combat from [3,2] to [2,2] -> Roman battle -> 1 7/7: End phase -> Carthaginian play one card -> 1 6/6: PlayCard(Order Heavy Troops) -> Carthaginian order 1 heavy units -> 1 " `; + +exports[`melee 1`] = ` +"600/2000: undefined -> Roman play one card -> 1 + 600/2000: PlayCard(Order Heavy Troops) -> Roman order 3 heavy units -> 4 + 216/666: OrderUnit([0,5],[1,5],[3,5]) -> Roman order 3 heavy units -> 1 + 216/666: End phase -> Roman movement -> 1 + 217/665: MacroCommand(Move [1,5] to [2,4],Move [0,5] to [1,4],Move [3,5] to [3,4]) -> Roman battle -> 5 + CHANCE/334: Close Combat from [3,4] to [3,3] -> Roman battle -> 3 + 66/90: Close Combat from [3,4] to [3,3] -> Roman attacker retreat -> 2 + -3/208: Close Combat from [3,4] to [3,3] -> Carthaginian retreat -> 3 + 31/35: Close Combat from [3,4] to [3,3] -> Roman attacker retreat -> 2 + CHANCE/151: Close Combat from [2,4] to [3,3] -> Roman battle -> 2 + 12/56: Close Combat from [2,4] to [3,3] -> Roman attacker retreat -> 2 + -1/94: Close Combat from [2,4] to [3,3] -> Carthaginian retreat -> 3 + CHANCE/70: Close Combat from [1,4] to [2,3] -> Roman battle -> 2 + -28/42: Close Combat from [1,4] to [2,3] -> Carthaginian retreat -> 2 + 19/27: Close Combat from [1,4] to [2,3] -> Roman attacker retreat -> 2 + CHANCE/31: Close Combat from [3,4] to [4,3] -> Roman battle -> 6 + -1/5: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + -12/13: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + -2/3: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 2 + 1/5: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 2 + 2/2: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 2 + -2/2: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + CHANCE/82: Close Combat from [2,4] to [2,3] -> Roman battle -> 2 + 18/34: Close Combat from [2,4] to [2,3] -> Roman attacker retreat -> 2 + -25/47: Close Combat from [2,4] to [2,3] -> Carthaginian retreat -> 2 + 116/422: OrderUnit([0,5],[1,5],[2,5]) -> Roman order 3 heavy units -> 1 + 116/422: End phase -> Roman movement -> 1 + 117/421: MacroCommand(Move [1,5] to [2,4],Move [2,5] to [3,4],Move [0,5] to [1,4]) -> Roman battle -> 5 + CHANCE/22: Close Combat from [1,4] to [2,3] -> Roman battle -> 2 + -9/13: Close Combat from [1,4] to [2,3] -> Carthaginian retreat -> 2 + -1/8: Close Combat from [1,4] to [2,3] -> Roman attacker retreat -> 2 + CHANCE/43: Close Combat from [2,4] to [2,3] -> Roman battle -> 2 + 10/18: Close Combat from [2,4] to [2,3] -> Roman attacker retreat -> 3 + -17/24: Close Combat from [2,4] to [2,3] -> Carthaginian retreat -> 2 + CHANCE/15: Close Combat from [3,4] to [4,3] -> Roman battle -> 5 + 1/1: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 2 + -2/2: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + -6/7: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + -1/2: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 2 + -2/2: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + CHANCE/58: Close Combat from [2,4] to [3,3] -> Roman battle -> 2 + 1/16: Close Combat from [2,4] to [3,3] -> Roman attacker retreat -> 3 + -4/41: Close Combat from [2,4] to [3,3] -> Carthaginian retreat -> 3 + CHANCE/286: Close Combat from [3,4] to [3,3] -> Roman battle -> 2 + 14/169: Close Combat from [3,4] to [3,3] -> Carthaginian retreat -> 3 + 81/116: Close Combat from [3,4] to [3,3] -> Roman attacker retreat -> 2 + 257/756: OrderUnit([0,5],[2,5],[3,5]) -> Roman order 3 heavy units -> 1 + 256/755: End phase -> Roman movement -> 1 + 257/754: MacroCommand(Move [2,5] to [2,4],Move [0,5] to [1,4],Move [3,5] to [3,4]) -> Roman battle -> 5 + CHANCE/32: Close Combat from [1,4] to [2,3] -> Roman battle -> 2 + -15/18: Close Combat from [1,4] to [2,3] -> Carthaginian retreat -> 2 + -3/13: Close Combat from [1,4] to [2,3] -> Roman attacker retreat -> 2 + CHANCE/215: Close Combat from [2,4] to [3,3] -> Roman battle -> 2 + 9/93: Close Combat from [2,4] to [3,3] -> Roman attacker retreat -> 2 + 2/121: Close Combat from [2,4] to [3,3] -> Carthaginian retreat -> 3 + CHANCE/75: Close Combat from [3,4] to [4,3] -> Roman battle -> 6 + -4/8: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + 2/12: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 2 + -6/6: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + -17/30: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + 5/12: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 2 + 2/6: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 2 + CHANCE/150: Close Combat from [2,4] to [2,3] -> Roman battle -> 2 + 17/62: Close Combat from [2,4] to [2,3] -> Roman attacker retreat -> 2 + -26/87: Close Combat from [2,4] to [2,3] -> Carthaginian retreat -> 2 + CHANCE/285: Close Combat from [3,4] to [3,3] -> Roman battle -> 3 + 62/87: Close Combat from [3,4] to [3,3] -> Roman attacker retreat -> 2 + -71/164: Close Combat from [3,4] to [3,3] -> Carthaginian retreat -> 3 + 28/33: Close Combat from [3,4] to [3,3] -> Roman attacker retreat -> 2 + 12/155: OrderUnit([1,5],[2,5],[3,5]) -> Roman order 3 heavy units -> 1 + 12/155: End phase -> Roman movement -> 1 + 11/154: MacroCommand(Move [1,5] to [2,4],Move [2,5] to [3,4],Move [3,5] to [4,4]) -> Roman battle -> 5 + CHANCE/14: Close Combat from [2,4] to [2,3] -> Roman battle -> 2 + 3/3: Close Combat from [2,4] to [2,3] -> Roman attacker retreat -> 2 + -8/10: Close Combat from [2,4] to [2,3] -> Carthaginian retreat -> 2 + CHANCE/42: Close Combat from [2,4] to [3,3] -> Roman battle -> 2 + 4/14: Close Combat from [2,4] to [3,3] -> Roman attacker retreat -> 2 + 3/27: Close Combat from [2,4] to [3,3] -> Carthaginian retreat -> 3 + CHANCE/40: Close Combat from [3,4] to [4,3] -> Roman battle -> 5 + 9/14: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 3 + 0/16: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + 1/1: Close Combat from [3,4] to [4,3] -> Roman attacker retreat -> 3 + 0/4: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + -4/4: Close Combat from [3,4] to [4,3] -> Carthaginian retreat -> 2 + CHANCE/13: Close Combat from [4,4] to [4,3] -> Roman battle -> 5 + -1/1: Close Combat from [4,4] to [4,3] -> Carthaginian retreat -> 2 + -3/3: Close Combat from [4,4] to [4,3] -> Carthaginian retreat -> 2 + -3/3: Close Combat from [4,4] to [4,3] -> Carthaginian retreat -> 2 + 2/4: Close Combat from [4,4] to [4,3] -> Roman attacker retreat -> 2 + 0/1: Close Combat from [4,4] to [4,3] -> Roman attacker retreat -> 2 + CHANCE/48: Close Combat from [3,4] to [3,3] -> Roman battle -> 2 + 18/30: Close Combat from [3,4] to [3,3] -> Carthaginian retreat -> 3 + -6/17: Close Combat from [3,4] to [3,3] -> Roman attacker retreat -> 3 +" +`; diff --git a/src/ai/mcts_player.test.js b/src/ai/mcts_player.test.js index 7b85d0e..5cbc31d 100644 --- a/src/ai/mcts_player.test.js +++ b/src/ai/mcts_player.test.js @@ -37,7 +37,7 @@ const player = new MctsPlayer({ }); // fails bc I don't know -xtest('2 on 2', () => { +test('2 on 2', () => { const game = makeGame(new TwoOnTwoMeleeScenario()); const root = player.search(game, 2000); @@ -46,8 +46,7 @@ xtest('2 on 2', () => { expect(rootAsString).toMatchSnapshot(); }); -// disabled bc of bug in simulation of battle back after ignored flags -xtest('melee', () => { +test('melee', () => { const game = makeGame(new MeleeScenario()); const root = player.search(game, 2000); @@ -56,8 +55,7 @@ xtest('melee', () => { expect(rootAsString).toMatchSnapshot(); }); -// disabled bc of evasion phase is not yet implemented -xtest('close combat from light to heavy', () => { +test('close combat from light to heavy', () => { const game = makeGame(new NullScenario()); game.placeUnit(hexOf(2, 2), new CarthaginianHeavyInfantry()); game.placeUnit(hexOf(2, 3), new RomanLightInfantry()); diff --git a/src/model/commands/abstract_combat_command.js b/src/model/commands/abstract_combat_command.js index 90f2b9c..699d6e8 100644 --- a/src/model/commands/abstract_combat_command.js +++ b/src/model/commands/abstract_combat_command.js @@ -55,7 +55,6 @@ export class AbstractCombatCommand extends Command { return [totalDamage, flagResult.retreats, diceResults]; } - /** * @param {Unit} attackingUnit * @param {Hex} defendingHex @@ -73,13 +72,13 @@ export class AbstractCombatCommand extends Command { const attackingHex = game.hexOfUnit(attackingUnit); if (game.isUnitDead(defendingUnit)) { events.push(new UnitKilledEvent(defendingHex, defendingUnit)); - if (!isBattleBack) { - game.unshiftPhase(new MomentumAdvancePhase(defendingHex, attackingHex)); - } + // if (!isBattleBack) { + // game.unshiftPhase(new MomentumAdvancePhase(defendingHex, attackingHex)); + // } } else if (retreatHexes.length > 0) { - if (!isBattleBack) { - game.unshiftPhase(new MomentumAdvancePhase(defendingHex, attackingHex)); - } + // if (!isBattleBack) { + // game.unshiftPhase(new MomentumAdvancePhase(defendingHex, attackingHex)); + // } const preventRecursiveBattleBack = isBattleBack ? null : attackingHex; game.unshiftPhase(new FirstDefenderRetreatPhase(preventRecursiveBattleBack, defendingUnit.side, defendingHex, retreatHexes)); } diff --git a/src/model/commands/ranged_combat_command.js b/src/model/commands/ranged_combat_command.js index 52289ae..09453f8 100644 --- a/src/model/commands/ranged_combat_command.js +++ b/src/model/commands/ranged_combat_command.js @@ -1,3 +1,6 @@ +import { DamageEvent, UnitKilledEvent } from "../events.js"; +import { FirstDefenderRetreatPhase } from "../phases/FirstDefenderRetreatPhase.js"; +import { RangedCombatRetreatPhase } from "../phases/RangedCombatRetreatPhase.js"; import { AbstractCombatCommand } from "./abstract_combat_command.js"; export class RangedCombatCommand extends AbstractCombatCommand { @@ -22,7 +25,18 @@ export class RangedCombatCommand extends AbstractCombatCommand { throw new Error(`Cannot Ranged Combat with unit at ${defendingHex} from ${attackingHex}`); } game.markUnitSpent(attackingUnit); - return this.attack(attackingUnit, defendingHex, defendingUnit, game); + + let totalDamage, retreatHexes, diceResults; + [totalDamage, retreatHexes, diceResults] = this.simpleAttack(attackingUnit, defendingHex, defendingUnit, game); + let events = []; + game.damageUnit(defendingUnit, totalDamage); + events.push(new DamageEvent(attackingUnit, defendingUnit, defendingHex, totalDamage, diceResults)); + if (game.isUnitDead(defendingUnit)) { + events.push(new UnitKilledEvent(defendingHex, defendingUnit)); + } else if (retreatHexes.length > 0) { + game.unshiftPhase(new RangedCombatRetreatPhase(defendingHex, retreatHexes)); + } + return events; } decideDiceCount(attackingUnit, game) { diff --git a/src/model/commands/skip_action_command.js b/src/model/commands/skip_action_command.js index 9689616..3ed1621 100644 --- a/src/model/commands/skip_action_command.js +++ b/src/model/commands/skip_action_command.js @@ -2,11 +2,11 @@ import { Command } from "./commands.js"; export class SkipActionCommand extends Command { /** - * @param {Hex} toHex + * @param {Hex} unitHex */ constructor(unitHex) { super(); - this.unitHex = unitHex; + this.toHex = unitHex; } play(game) { @@ -15,6 +15,6 @@ export class SkipActionCommand extends Command { } toString() { - return `Skip action unit at ${this.unitHex}`; + return `Skip action unit at ${this.toHex}`; } } diff --git a/src/model/phases/RangedCombatRetreatPhase.js b/src/model/phases/RangedCombatRetreatPhase.js new file mode 100644 index 0000000..90deb79 --- /dev/null +++ b/src/model/phases/RangedCombatRetreatPhase.js @@ -0,0 +1,36 @@ +import { Hex } from "../../lib/hexlib.js"; +import { RetreatCommand } from "../commands/retreatCommand.js"; +import { Phase } from "./Phase.js"; + + +export class RangedCombatRetreatPhase extends Phase { + /** + * @param {Hex} defendingHex + * @param {Hex[]} retreatHexes + */ + constructor(defendingHex, retreatHexes) { + super("retreat"); + this.fromHex = defendingHex; + this.retreatHexes = retreatHexes; + } + + validCommands(game) { + return this.retreatHexes.map(toHex => { + return new RetreatCommand(toHex, this.fromHex); + }); + } + + hilightedHexes(game) { + return new Set(this.retreatHexes); + } + + /** + * @param {Hex} hex + * @param {InteractiveGame} interactiveGame + * @returns {Command|undefined} + */ + onClick(hex, interactiveGame) { + return this.validCommands(interactiveGame). + find(command => command["toHex"] === hex || command["battleBackHex"] === hex); + } +}