| """Engine rules tests: costs, sigils, scales, foe machinery.""" |
|
|
| import pytest |
|
|
| from scrypt.engine.cards import Card, CardInstance, Cost, CostType, make_card |
| from scrypt.engine.combat import ( |
| LANES, |
| CombatState, |
| IllegalMove, |
| Phase, |
| Result, |
| ScriptedPlay, |
| ) |
|
|
| BIT = make_card("bit", health=1) |
| DAEMON = make_card("daemon", power=1, health=2, cost=Cost(CostType.MEM, 1)) |
| BRUTE = make_card("brute", power=3, health=2, cost=Cost(CostType.MEM, 2)) |
| FLYER = make_card("flyer", power=2, health=1, cost=Cost(CostType.MEM, 1), sigils=("tunneling",)) |
| WALL = make_card("wall", power=0, health=4, cost=Cost(CostType.MEM, 1), sigils=("packet_filter",)) |
| SPLITTER = make_card("splitter", power=1, health=1, sigils=("forked",)) |
| VENOM = make_card("venom", power=1, health=1, cost=Cost(CostType.MEM, 2), sigils=("null_pointer",)) |
| SPIKES = make_card("spikes", power=0, health=3, sigils=("honeypot",)) |
| GOAT = make_card("goat", power=0, health=1, cost=Cost(CostType.MEM, 1), sigils=("privileged",)) |
| CAT = make_card("cat", power=0, health=1, cost=Cost(CostType.MEM, 1), sigils=("auto_restart",)) |
| BREEDER = make_card("breeder", power=1, health=1, cost=Cost(CostType.MEM, 1), sigils=("self_replicating",)) |
| DIGGER = make_card("digger", power=0, health=3, sigils=("scavenger_loop",)) |
| BONEY = make_card("boney", power=2, health=3, cost=Cost(CostType.DUMPS, 4)) |
|
|
|
|
| def fresh(script=None, main=None, side=None, seed=7) -> CombatState: |
| return CombatState( |
| main_deck=main if main is not None else [DAEMON] * 6, |
| side_deck=side if side is not None else [BIT] * 10, |
| script=script or [[]], |
| seed=seed, |
| ) |
|
|
|
|
| def in_main(state: CombatState) -> CombatState: |
| """Skip past the draw phase for board-setup tests.""" |
| if state.phase is Phase.DRAW: |
| state.draw("side" if state.can_draw_side else "main") |
| return state |
|
|
|
|
| def put(state, row, lane, card_spec): |
| inst = CardInstance(spec=card_spec) |
| getattr(state, row)[lane] = inst |
| return inst |
|
|
|
|
| |
|
|
| def test_opening_hand_is_three_main_plus_one_bit(): |
| state = fresh() |
| assert len(state.hand) == 4 |
| assert sum(1 for c in state.hand if c.spec.id == "bit") == 1 |
|
|
|
|
| def test_turn_zero_script_reaches_front_row(): |
| raider = make_card("raider", power=1, health=1) |
| state = fresh(script=[[ScriptedPlay(1, raider)], [ScriptedPlay(2, raider)]]) |
| assert state.foe_row[1] is not None and state.foe_row[1].spec.id == "raider" |
| assert state.foe_queue[2] is not None |
|
|
|
|
| |
|
|
| def test_free_card_plays_without_sacrifice(): |
| state = in_main(fresh()) |
| state.hand.append(CardInstance(spec=BIT)) |
| state.play(len(state.hand) - 1, 0) |
| assert state.player_row[0].spec.id == "bit" |
|
|
|
|
| def test_mem_cost_requires_sacrifice(): |
| state = in_main(fresh()) |
| state.hand.append(CardInstance(spec=DAEMON)) |
| with pytest.raises(IllegalMove): |
| state.play(len(state.hand) - 1, 0) |
|
|
|
|
| def test_sacrifice_pays_mem_and_grants_dumps(): |
| state = in_main(fresh()) |
| put(state, "player_row", 2, BIT) |
| state.hand.append(CardInstance(spec=DAEMON)) |
| state.play(len(state.hand) - 1, 0, sacrifices=(2,)) |
| assert state.player_row[2] is None |
| assert state.player_row[0].spec.id == "daemon" |
| assert state.dumps == 1 |
|
|
|
|
| def test_can_play_into_lane_of_sacrificed_card(): |
| state = in_main(fresh()) |
| put(state, "player_row", 1, BIT) |
| state.hand.append(CardInstance(spec=DAEMON)) |
| state.play(len(state.hand) - 1, 1, sacrifices=(1,)) |
| assert state.player_row[1].spec.id == "daemon" |
|
|
|
|
| def test_cannot_play_into_occupied_lane(): |
| state = in_main(fresh()) |
| put(state, "player_row", 1, BIT) |
| state.hand.append(CardInstance(spec=BIT)) |
| with pytest.raises(IllegalMove): |
| state.play(len(state.hand) - 1, 1) |
|
|
|
|
| def test_over_sacrifice_rejected(): |
| state = in_main(fresh()) |
| put(state, "player_row", 0, BIT) |
| put(state, "player_row", 1, BIT) |
| state.hand.append(CardInstance(spec=DAEMON)) |
| with pytest.raises(IllegalMove, match="over-sacrifice"): |
| state.play(len(state.hand) - 1, 2, sacrifices=(0, 1)) |
|
|
|
|
| def test_privileged_is_worth_three(): |
| state = in_main(fresh()) |
| put(state, "player_row", 0, GOAT) |
| state.hand.append(CardInstance(spec=BRUTE)) |
| state.play(len(state.hand) - 1, 1, sacrifices=(0,)) |
| assert state.player_row[1].spec.id == "brute" |
|
|
|
|
| def test_auto_restart_survives_sacrifice(): |
| state = in_main(fresh()) |
| cat = put(state, "player_row", 0, CAT) |
| state.hand.append(CardInstance(spec=DAEMON)) |
| state.play(len(state.hand) - 1, 1, sacrifices=(0,)) |
| assert state.player_row[0] is cat |
| assert state.dumps == 0 |
|
|
|
|
| def test_dump_cost_paid_from_pool(): |
| state = in_main(fresh()) |
| state.dumps = 4 |
| state.hand.append(CardInstance(spec=BONEY)) |
| state.play(len(state.hand) - 1, 0) |
| assert state.dumps == 0 |
| state.hand.append(CardInstance(spec=BONEY)) |
| with pytest.raises(IllegalMove): |
| state.play(len(state.hand) - 1, 1) |
|
|
|
|
| def test_self_replicating_adds_copy_to_hand(): |
| state = in_main(fresh()) |
| put(state, "player_row", 0, BIT) |
| state.hand.append(CardInstance(spec=BREEDER)) |
| before = len(state.hand) |
| state.play(len(state.hand) - 1, 1, sacrifices=(0,)) |
| assert len(state.hand) == before |
| assert state.hand[-1].spec.id == "breeder" |
|
|
|
|
| |
|
|
| def test_unblocked_attack_tips_scale(): |
| state = in_main(fresh()) |
| put(state, "player_row", 0, BRUTE) |
| state.ring_bell() |
| assert state.scale == 3 |
|
|
|
|
| def test_win_at_five_with_overkill_cycles(): |
| state = in_main(fresh()) |
| big = make_card("big", power=9, health=1) |
| put(state, "player_row", 0, big) |
| state.ring_bell() |
| assert state.result is Result.PLAYER_WIN |
| assert state.scale == 5 |
| assert state.overkill_cycles == 4 |
|
|
|
|
| def test_loss_at_minus_five(): |
| state = in_main(fresh()) |
| crusher = make_card("crusher", power=5, health=5) |
| put(state, "foe_row", 0, crusher) |
| state.ring_bell() |
| assert state.result is Result.PLAYER_LOSS |
|
|
|
|
| def test_blocked_attack_damages_card_not_scale(): |
| state = in_main(fresh()) |
| put(state, "player_row", 1, BRUTE) |
| foe = put(state, "foe_row", 1, make_card("meat", power=0, health=5)) |
| state.ring_bell() |
| assert foe.health == 2 |
| assert state.scale == 0 |
|
|
|
|
| def test_tunneling_flies_over_blocker(): |
| state = in_main(fresh()) |
| put(state, "player_row", 0, FLYER) |
| put(state, "foe_row", 0, make_card("meat", power=0, health=5)) |
| state.ring_bell() |
| assert state.scale == 2 |
|
|
|
|
| def test_packet_filter_blocks_tunneling(): |
| state = in_main(fresh()) |
| put(state, "player_row", 0, FLYER) |
| wall = put(state, "foe_row", 0, WALL) |
| state.ring_bell() |
| assert state.scale == 0 |
| assert wall.health == 2 |
|
|
|
|
| def test_forked_hits_adjacent_lanes_only(): |
| state = in_main(fresh()) |
| put(state, "player_row", 1, SPLITTER) |
| left = put(state, "foe_row", 0, make_card("l", power=0, health=2)) |
| mid = put(state, "foe_row", 1, make_card("m", power=0, health=2)) |
| right = put(state, "foe_row", 2, make_card("r", power=0, health=2)) |
| state.ring_bell() |
| assert left.health == 1 and right.health == 1 |
| assert mid.health == 2 |
|
|
|
|
| def test_null_pointer_kills_any_size(): |
| state = in_main(fresh()) |
| put(state, "player_row", 0, VENOM) |
| put(state, "foe_row", 0, make_card("giant", power=0, health=99)) |
| state.ring_bell() |
| assert state.foe_row[0] is None |
|
|
|
|
| def test_honeypot_recoil_kills_fragile_attacker(): |
| state = in_main(fresh()) |
| put(state, "player_row", 0, make_card("soft", power=1, health=1)) |
| put(state, "foe_row", 0, SPIKES) |
| state.ring_bell() |
| assert state.player_row[0] is None |
| assert state.dumps == 1 |
|
|
|
|
| def test_player_death_grants_dumps_foe_death_does_not(): |
| state = in_main(fresh()) |
| put(state, "player_row", 0, BRUTE) |
| put(state, "foe_row", 0, make_card("meat", power=9, health=1)) |
| state.ring_bell() |
| |
| assert state.foe_row[0] is None |
| assert state.dumps == 0 |
|
|
|
|
| def test_scavenger_loop_yields_bone_each_turn(): |
| state = in_main(fresh()) |
| put(state, "player_row", 3, DIGGER) |
| state.ring_bell() |
| assert state.dumps == 1 |
|
|
|
|
| def test_queue_advances_after_foe_turn(): |
| raider = make_card("raider", power=0, health=1) |
| state = in_main(fresh(script=[[], [ScriptedPlay(0, raider)], []])) |
| assert state.foe_queue[0] is not None |
| state.ring_bell() |
| assert state.foe_row[0] is not None |
| assert state.foe_queue[0] is None |
|
|
|
|
| def test_draw_phase_enforced(): |
| state = fresh() |
| with pytest.raises(IllegalMove): |
| state.ring_bell() |
| state.draw("main") |
| assert state.phase is Phase.MAIN |
|
|