class TestMovementAction(unittest.TestCase): def setUp(self): collect() shape = (16, 16) self.board = GameBoard(shape) self.state = GameState(shape) self.tank = buildFigure('Tank', (8, 8), RED) self.state.addFigure(self.tank) def testMoveToDestination(self): dst = Hex(4, 4).cube() move = GM.actionMove(self.board, self.state, self.tank, destination=dst) GM.step(self.board, self.state, move) self.assertEqual( self.state.getFiguresByPos(move.team, move.destination)[0], self.tank, 'figure in the wrong position') self.assertEqual(self.tank.stat, stat('IN_MOTION'), 'figure should be in motion') self.assertEqual(self.tank.position, dst, 'figure not at the correct destination') def testMoveOnRoad(self): shape = (1, 16) board = GameBoard(shape) t = buildFigure('Tank', (0, 0), RED) i = buildFigure('Infantry', (0, 15), RED) stateTank = GameState(shape) stateTank.addFigure(t) stateInf = GameState(shape) stateInf.addFigure(i) # movements without road nTankNoRoad = len(GM.buildMovements(board, stateTank, t)) nInfNoRoad = len(GM.buildMovements(board, stateInf, i)) # adding road road = np.zeros(shape, 'uint8') road[0, :] = TERRAIN_TYPE['ROAD'].level board.addTerrain(road) # test for vehicles nTankRoad = len(GM.buildMovements(board, stateTank, t)) nInfRoad = len(GM.buildMovements(board, stateInf, i)) # tank self.assertNotEqual(nTankRoad, nTankNoRoad, 'road has no influence for tank') self.assertEqual(nTankRoad, 8, 'invalid distance with road for tank') self.assertEqual(nTankNoRoad, 6, 'invalid distance without road for tank') self.assertEqual(nTankRoad - nTankNoRoad, 2, 'road does not increase by 2 the distance for tank') # infantry self.assertNotEqual(nInfRoad, nInfNoRoad, 'road has no influence on infantry') self.assertEqual(nInfRoad, 4, 'invalid distance with road for infantry') self.assertEqual(nInfNoRoad, 3, 'invalid distance without road for infantry') self.assertEqual( nInfRoad - nInfNoRoad, 1, 'road does not increase by 1 the distance for infantry') # test for road change board.terrain[0, 0] = 0 board.terrain[0, 15] = 0 nTankRoad = len(GM.buildMovements(board, stateTank, t)) nInfRoad = len(GM.buildMovements(board, stateInf, i)) self.assertEqual(nTankRoad, 8, 'invalid distance for tank') self.assertEqual(nInfRoad, 4, 'invalid distance for infantry') def testActivateMoveToDestination(self): dst = Hex(4, 4).cube() move = GM.actionMove(self.board, self.state, self.tank, destination=dst) self.state1, _ = GM.activate(self.board, self.state, move) self.assertNotEqual( hash(self.state1), hash(self.state), 'self.state1 and self.state0 are the same, should be different!') self.assertNotEqual( self.state.getFigure(move).position, self.state1.getFigure(move).position, 'figure is in teh same location for both self.state0 and self.state1!' ) self.state2, _ = GM.activate(self.board, self.state, move) self.assertNotEqual( hash(self.state2), hash(self.state), 'self.state2 and self.state0 are the same, should be different!') self.assertEqual( self.state1.getFigure(move).position, self.state2.getFigure(move).position, 'self.state1 and self.state2 have different end location') self.state3, _ = GM.activate(self.board, self.state, move) self.assertNotEqual( hash(self.state3), hash(self.state), 'self.state3 and self.state0 are the same, should be different!') self.assertEqual( self.state1.getFigure(move).position, self.state3.getFigure(move).position, 'self.state1 and self.state3 have different end location') self.assertEqual( self.state2.getFigure(move).position, self.state3.getFigure(move).position, 'self.state2 and self.state3 have different end location') def testMoveWithTransport(self): inf1 = buildFigure('Infantry', (7, 7), RED, 'Inf1', stat('NO_EFFECT')) inf2 = buildFigure('Infantry', (7, 8), RED, 'Inf2', stat('NO_EFFECT')) inf3 = buildFigure('Infantry', (7, 9), RED, 'Inf3', stat('NO_EFFECT')) # add infantry units self.state.addFigure(inf1) self.state.addFigure(inf2) self.state.addFigure(inf3) # load 2 units load1 = GM.actionLoadInto(self.board, self.state, inf1, self.tank) load2 = GM.actionLoadInto(self.board, self.state, inf2, self.tank) load3 = GM.actionLoadInto(self.board, self.state, inf3, self.tank) GM.step(self.board, self.state, load1) GM.step(self.board, self.state, load2) # load a third unit: cannot do that! self.assertRaises(ValueError, GM.step, self.board, self.state, load3) self.assertEqual(inf1.position, self.tank.position) self.assertEqual(inf2.position, self.tank.position) self.assertNotEqual(inf3.position, self.tank.position) # move figure in same position of tank move = GM.actionMove(self.board, self.state, inf3, destination=self.tank.position) GM.step(self.board, self.state, move) figures = self.state.getFiguresByPos(RED, self.tank.position) self.assertEqual(len(figures), 4, 'not all figures are in the same position') self.assertEqual(inf1.transported_by, self.tank.index, 'Inf1 not in transporter') self.assertEqual(inf2.transported_by, self.tank.index, 'Inf2 not in transporter') self.assertEqual(inf3.transported_by, -1, 'Inf3 is in transporter') # move tank dst = Hex(8, 2).cube() move = GM.actionMove(self.board, self.state, self.tank, destination=dst) GM.step(self.board, self.state, move) # figures moves along with tank self.assertEqual(inf1.position, self.tank.position, 'Inf1 not moved with transporter') self.assertEqual(inf2.position, self.tank.position, 'Inf2 not moved with transporter') self.assertEqual(len(self.tank.transporting), 2, 'Transporter not transporting all units') self.assertGreater(inf1.transported_by, -1) # unload 1 figure dst = Hex(8, 4).cube() move = GM.actionMove(self.board, self.state, inf1, destination=dst) GM.step(self.board, self.state, move) self.assertEqual(len(self.tank.transporting), 1, 'transporter has less units than expected') self.assertNotEqual( inf1.position, self.tank.position, 'Inf1 has not been moved together with transporter') def testMoveInsideShape(self): # top left dst = Hex(0, 0).cube() self.state.moveFigure(self.tank, dst=dst) moves = GM.buildMovements(self.board, self.state, self.tank) for move in moves: d: Cube = move.destination x, y = d.tuple() self.assertGreaterEqual(x, 0, f'moves outside of map limits: ({x},{y})') self.assertGreaterEqual(y, 0, f'moves outside of map limits: ({x},{y})') # bottom right dst = Hex(15, 15).cube() self.state.moveFigure(self.tank, dst=dst) moves = GM.buildMovements(self.board, self.state, self.tank) for move in moves: d: Cube = move.destination x, y = d.tuple() self.assertLess(x, 16, f'moves outside of map limits: ({x},{y})') self.assertLess(y, 16, f'moves outside of map limits: ({x},{y})') def testMoveOutsideShape(self): # outside of map dst = Hex(-1, -1).cube() self.state.moveFigure(self.tank, dst=dst) moves = GM.buildMovements(self.board, self.state, self.tank) self.assertEqual(len(moves), 0, 'moves outside of the map!')
def step(self, board: GameBoard, state: GameState, action: Action, forceHit: bool = False) -> Outcome: """Update the given state with the given action in a irreversible way.""" team: str = action.team # team performing action comment: str = '' logger.debug(f'{team} step with {action}') state.lastAction = action if isinstance(action, Wait): logger.debug(f'{action}: {comment}') return Outcome(comment=comment) if isinstance(action, Pass): if isinstance(action, PassFigure): f: Figure = state.getFigure(action) # who performs the action f.activated = True f.passed = True if isinstance(action, PassTeam) and not isinstance(action, Response): for f in state.getFigures(team): f.activated = True f.passed = True logger.debug(f'{action}: {comment}') return Outcome(comment=comment) if isinstance(action, Move): f: Figure = state.getFigure(action) # who performs the action f.activated = True f.moved = True f.stat = stat('IN_MOTION') if isinstance(action, MoveLoadInto): # figure moves inside transporter t = state.getTransporter(action) t.transportLoad(f) comment = f'(capacity: {len(t.transporting)}/{t.transport_capacity})' elif f.transported_by > -1: # figure leaves transporter t = state.getFigureByIndex(team, f.transported_by) t.transportUnload(f) comment = f'(capacity: {len(t.transporting)}/{t.transport_capacity})' state.moveFigure(f, f.position, action.destination) for transported in f.transporting: t = state.getFigureByIndex(team, transported) t.stat = stat('LOADED') state.moveFigure(t, t.position, action.destination) logger.debug(f'{action}: {comment}') return Outcome(comment=comment) if isinstance(action, AttackGround): f: Figure = state.getFigure(action) # who performs the action x: Cube = action.ground w: Weapon = state.getWeapon(action) f.stat = stat('NO_EFFECT') f.activated = True f.attacked = True w.shoot() if w.smoke: cloud = [ x + Cube(0, -1, 1), x + Cube(1, -1, 0), x + Cube(1, 0, -1), x + Cube(0, 1, -1), x + Cube(-1, 1, 0), x + Cube(-1, 0, 1), ] cloud = [(c.distance(f.position), c) for c in cloud] cloud = sorted(cloud, key=lambda y: -y[0]) state.addSmoke([c[1] for c in cloud[1:3]] + [x]) comment = f'smoke at {x}' logger.debug(f'{action}: {comment}') return Outcome(comment=comment) if isinstance(action, Attack): # Respond *is* an attack action f: Figure = state.getFigure(action) # who performs the action t: Figure = state.getTarget(action) # target # g: Figure = action.guard # who has line-of-sight on target w: Weapon = state.getWeapon(action) # los: list = action.los # line-of-sight on target of guard lof: list = action.lof # line-of-fire on target of figure # consume ammunition f.stat = stat('NO_EFFECT') w.shoot() if forceHit: score = [0] * w.dices else: score = np.random.choice(range(1, 21), size=w.dices) # attack/response if isinstance(action, Response): ATK = w.atk_response INT = f.int_def # can respond only once in a turn f.responded = True else: ATK = w.atk_normal INT = f.int_atk f.activated = True f.attacked = True # anti-tank rule if state.hasSmoke(lof): DEF = t.defense['smoke'] elif w.antitank and t.kind == 'vehicle': DEF = t.defense['antitank'] else: DEF = t.defense['basic'] TER = board.getProtectionLevel(t.position) STAT = f.stat.value + f.bonus END = f.endurance hitScore = hitScoreCalculator(ATK, TER, DEF, STAT, END, INT) success = len([x for x in score if x <= hitScore]) # target status changes for the _next_ hit t.stat = stat('UNDER_FIRE') # target can now respond to the fire t.attacked_by = f.index if success > 0: self.applyDamage(state, action, hitScore, score, success, t, w) comment = f'success=({success} {score}/{hitScore}) target=({t.hp}/{t.hp_max})' if t.hp <= 0: comment += ' KILLED!' elif w.curved: # missing with curved weapons v = np.random.choice(range(1, 21), size=1) hitLocation = MISS_MATRIX[team](v) missed = state.getFiguresByPos(t.team, hitLocation) missed = [m for m in missed if not m.killed] comment = f'({success} {score}/{hitScore}): shell missed and hit {hitLocation}: {len(missed)} hit' for m in missed: self.applyDamage(state, action, hitScore, score, 1, m, w) else: logger.debug(f'({success} {score}/{hitScore}): MISS!') logger.debug(f'{action}: {comment}') return Outcome( comment=comment, score=score, hitScore=hitScore, ATK=ATK, TER=TER, DEF=DEF, STAT=STAT, END=END, INT=INT, success=success > 0, hits=success, )