class TestAttackAction(unittest.TestCase): def setUp(self): collect() shape = (16, 16) self.board = GameBoard(shape) self.state = GameState(shape) self.red_tank = buildFigure('Tank', (0, 6), RED) self.red_inf = buildFigure('Infantry', (0, 12), RED) self.blue_tank = buildFigure('Tank', (15, 6), BLUE) self.blue_inf = buildFigure('Infantry', (15, 12), BLUE) self.state.addFigure(self.red_tank, self.red_inf, self.blue_tank, self.blue_inf) def testAttack(self): attack = GM.actionAttack(self.board, self.state, self.red_tank, self.blue_tank, self.red_tank.weapons['CA']) target = self.state.getTarget(attack) weapon = self.state.getWeapon(attack) o = GM.step(self.board, self.state, attack, True) self.assertTrue(o.success, 'failed to attack target') self.assertTrue(target.killed, 'target still alive') self.assertEqual(target.hp, target.hp_max - 1, 'no damage to the target') self.assertEqual(weapon.ammo, weapon.ammo_max - 1, 'shell not fired') def testActivateAttack(self): atk = GM.actionAttack(self.board, self.state, self.red_tank, self.blue_tank, self.red_tank.weapons['CA']) t0 = self.state.getTarget(atk) w0 = self.state.getWeapon(atk) s1, _ = GM.activate(self.board, self.state, atk, True) s2, _ = GM.activate(self.board, self.state, atk, True) self.assertNotEqual(hash(self.state), hash(s1), 'state1 and state0 are the same') self.assertNotEqual(hash(self.state), hash(s2), 'state2 and state0 are the same') t1 = s1.getTarget(atk) w1 = s1.getWeapon(atk) self.assertNotEqual(t0.killed, t1.killed, 'both target have the same status') self.assertFalse(t0.killed, 'target for state0 has been killed') self.assertTrue(t1.killed, 'target for state1 is still alive') self.assertEqual(w0.ammo - 1, w1.ammo, 'shots fired in the wrong state') def testShootingGround(self): ground = Hex(2, 6).cube() attack = GM.actionAttackGround(self.red_tank, ground, self.red_tank.weapons['SG']) GM.step(self.board, self.state, attack) self.assertEqual(self.state.smoke.max(), 2, 'cloud with wrong value') self.assertEqual(self.state.smoke.sum(), 6, 'not enough hex have cloud') GM.update(self.state) self.assertEqual(self.state.smoke.max(), 1, 'cloud decay not working') atk = GM.actionAttack(self.board, self.state, self.blue_tank, self.red_tank, self.red_tank.weapons['CA']) outcome = GM.step(self.board, self.state, atk) self.assertGreaterEqual(outcome.DEF, 18, 'smoke defense not active') GM.update(self.state) GM.update(self.state) self.assertEqual(self.state.smoke.max(), 0, 'cloud not disappearing correctly') def testDisableWeapon(self): # TODO pass
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, )