Scrypt / tests /test_combat.py
IMJONEZZ's picture
SCRYPT: initial commit — game, sandbox, Warden, Space web layer
9fca766
Raw
History Blame Contribute Delete
9.01 kB
"""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