Spaces:
Running on Zero
Running on Zero
| """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 | |
| # ----------------------------------------------------------------- setup | |
| 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 | |
| # ----------------------------------------------------------------- costs | |
| 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)) # costs 1 mem | |
| 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)) # costs 2 mem | |
| 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 # no death, no dump | |
| 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 # played one, gained one | |
| assert state.hand[-1].spec.id == "breeder" | |
| # ----------------------------------------------------------------- combat | |
| 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 # player's card died | |
| def test_player_death_grants_dumps_foe_death_does_not(): | |
| state = in_main(fresh()) | |
| put(state, "player_row", 0, BRUTE) # kills foe meat: no dumps | |
| put(state, "foe_row", 0, make_card("meat", power=9, health=1)) # kills brute back? no — simultaneous? sequential. | |
| state.ring_bell() | |
| # Brute (3 power) kills meat (1 health) before meat ever swings. | |
| 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 | |