221 lines
7.7 KiB
Python
221 lines
7.7 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from engine.character import Character
|
|
from engine.defs import DefRegistry
|
|
from engine.map import Map
|
|
from engine.tile import Tile, TileLayerInstance
|
|
from engine.world import World
|
|
from resources.logic.hacking import find_hacking_implant, roll_tech_check, try_hack_tile_layer
|
|
|
|
DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs"
|
|
|
|
|
|
@pytest.fixture()
|
|
def registry():
|
|
return DefRegistry.load(DEFS_DIR)
|
|
|
|
|
|
class FixedRng:
|
|
"""Deterministic stand-in for random.Random: fixed die roll."""
|
|
|
|
def __init__(self, roll: int):
|
|
self._roll = roll
|
|
|
|
def randint(self, _lo, _hi):
|
|
return self._roll
|
|
|
|
def choice(self, seq):
|
|
return seq[0]
|
|
|
|
def uniform(self, a, b):
|
|
return a
|
|
|
|
|
|
def make_human(registry, **kwargs) -> Character:
|
|
return Character(species_id="human", default_body_parts=registry.new_default_body_parts("human"), **kwargs)
|
|
|
|
|
|
def make_world_with_locked_door(registry) -> World:
|
|
field = Map("field", 5, 5, 1)
|
|
for x in range(5):
|
|
for y in range(5):
|
|
field.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("grass")))
|
|
field.set_tile(2, 2, 0, Tile(flooring=TileLayerInstance("grass"), room=TileLayerInstance("door_locked")))
|
|
world = World(registry=registry)
|
|
world.add_map(field)
|
|
return world
|
|
|
|
|
|
# --- base_tech_skill / roll_tech_check -------------------------------------------------------
|
|
|
|
|
|
def test_base_tech_skill_averages_intelligence_and_perception(registry):
|
|
human = make_human(registry)
|
|
human.bio["intelligence"] = 4
|
|
human.bio["perception"] = 2
|
|
assert human.base_tech_skill() == 3.0
|
|
|
|
|
|
def test_base_tech_skill_is_zero_by_default(registry):
|
|
human = make_human(registry)
|
|
assert human.base_tech_skill() == 0.0
|
|
|
|
|
|
def test_tech_is_not_a_directly_buyable_skill(registry):
|
|
human = make_human(registry)
|
|
assert "tech" not in human.bio
|
|
|
|
|
|
def test_roll_tech_check_matches_the_derived_base_skill_with_no_penalties(registry):
|
|
human = make_human(registry)
|
|
human.bio["intelligence"] = 4
|
|
human.bio["perception"] = 2
|
|
roll, effective_skill = roll_tech_check(human, registry, FixedRng(roll=11))
|
|
assert roll == 11
|
|
assert effective_skill == 3.0
|
|
|
|
|
|
# --- try_hack_tile_layer -----------------------------------------------------------------------
|
|
|
|
|
|
def test_hack_succeeds_and_swaps_the_layer_to_its_unlocked_def(registry):
|
|
world = make_world_with_locked_door(registry)
|
|
human = make_human(registry)
|
|
human.bio["intelligence"] = 10
|
|
human.bio["perception"] = 10 # base_tech_skill = 10, easily clears the door's DC 12
|
|
|
|
assert try_hack_tile_layer(human, world, registry, "field", 2, 2, 0, "room", rng=FixedRng(roll=10)) is True
|
|
|
|
tile = world.maps["field"].get_tile(2, 2, 0)
|
|
assert tile.room.def_id == "door_open"
|
|
assert tile.is_walkable(registry) is True
|
|
|
|
|
|
def test_hack_fails_a_roll_too_low_and_leaves_the_door_locked(registry):
|
|
world = make_world_with_locked_door(registry)
|
|
human = make_human(registry) # base_tech_skill = 0
|
|
|
|
assert try_hack_tile_layer(human, world, registry, "field", 2, 2, 0, "room", rng=FixedRng(roll=1)) is False
|
|
|
|
tile = world.maps["field"].get_tile(2, 2, 0)
|
|
assert tile.room.def_id == "door_locked"
|
|
assert tile.is_walkable(registry) is False
|
|
|
|
|
|
def test_hack_is_a_no_op_on_a_non_hackable_layer(registry):
|
|
world = make_world_with_locked_door(registry)
|
|
world.maps["field"].get_tile(2, 2, 0).room = TileLayerInstance("wood_wall")
|
|
human = make_human(registry)
|
|
human.bio["intelligence"] = 10
|
|
human.bio["perception"] = 10
|
|
|
|
assert try_hack_tile_layer(human, world, registry, "field", 2, 2, 0, "room", rng=FixedRng(roll=20)) is False
|
|
|
|
tile = world.maps["field"].get_tile(2, 2, 0)
|
|
assert tile.room.def_id == "wood_wall"
|
|
|
|
|
|
def test_hack_is_a_no_op_on_an_empty_layer(registry):
|
|
world = make_world_with_locked_door(registry)
|
|
human = make_human(registry)
|
|
|
|
assert try_hack_tile_layer(human, world, registry, "field", 0, 0, 0, "room", rng=FixedRng(roll=20)) is False
|
|
|
|
|
|
# --- hacking implants (cyberdecks) --------------------------------------------------------------
|
|
|
|
|
|
def test_find_hacking_implant_returns_none_without_one(registry):
|
|
human = make_human(registry)
|
|
assert find_hacking_implant(human, registry) is None
|
|
|
|
|
|
def test_find_hacking_implant_returns_the_installed_cyberdeck(registry):
|
|
human = make_human(registry)
|
|
human.install_implant("cyberdeck_biomechanics_mk1", registry)
|
|
implant = find_hacking_implant(human, registry)
|
|
assert implant is not None
|
|
assert implant.id == "cyberdeck_biomechanics_mk1"
|
|
|
|
|
|
def test_biomechanics_mk1_adds_a_plus_one_bonus_with_no_roll_floor(registry):
|
|
human = make_human(registry)
|
|
human.install_implant("cyberdeck_biomechanics_mk1", registry)
|
|
roll, effective_skill = roll_tech_check(human, registry, FixedRng(roll=1))
|
|
assert roll == 1 # no floor - a bad roll stays bad
|
|
assert effective_skill == 1.0 # 0 base + 1 bonus
|
|
|
|
|
|
def test_biomechanics_mk2_adds_a_plus_two_bonus(registry):
|
|
human = make_human(registry)
|
|
human.install_implant("cyberdeck_biomechanics_mk2", registry)
|
|
_roll, effective_skill = roll_tech_check(human, registry, FixedRng(roll=1))
|
|
assert effective_skill == 2.0
|
|
|
|
|
|
def test_infoinc_mk1_applies_penalty_but_floors_the_roll_at_45_percent(registry):
|
|
human = make_human(registry)
|
|
human.install_implant("cyberdeck_infoinc_mk1", registry)
|
|
roll, effective_skill = roll_tech_check(human, registry, FixedRng(roll=1))
|
|
assert roll == 9 # round(0.45 * 20)
|
|
assert effective_skill == -3.0
|
|
|
|
|
|
def test_infoinc_mk1_does_not_lower_an_already_better_roll(registry):
|
|
human = make_human(registry)
|
|
human.install_implant("cyberdeck_infoinc_mk1", registry)
|
|
roll, _effective_skill = roll_tech_check(human, registry, FixedRng(roll=15))
|
|
assert roll == 15 # already above the 9-roll floor - left alone
|
|
|
|
|
|
def test_infoinc_mk2_has_no_bonus_and_no_roll_floor(registry):
|
|
human = make_human(registry)
|
|
human.install_implant("cyberdeck_infoinc_mk2", registry)
|
|
roll, effective_skill = roll_tech_check(human, registry, FixedRng(roll=1))
|
|
assert roll == 1
|
|
assert effective_skill == 0.0
|
|
|
|
|
|
def test_infoinc_mk3_adds_a_plus_two_bonus_and_floors_the_roll_at_60_percent(registry):
|
|
human = make_human(registry)
|
|
human.install_implant("cyberdeck_infoinc_mk3", registry)
|
|
roll, effective_skill = roll_tech_check(human, registry, FixedRng(roll=1))
|
|
assert roll == 12 # round(0.6 * 20)
|
|
assert effective_skill == 2.0
|
|
|
|
|
|
def test_the_miro_secret_cyberdeck_adds_a_plus_five_bonus_and_floors_the_roll_at_75_percent(registry):
|
|
human = make_human(registry)
|
|
human.install_implant("cyberdeck_the_miro_model_3a", registry)
|
|
roll, effective_skill = roll_tech_check(human, registry, FixedRng(roll=1))
|
|
assert roll == 15 # round(0.75 * 20)
|
|
assert effective_skill == 5.0
|
|
|
|
|
|
def test_the_miro_secret_cyberdeck_is_marked_secret(registry):
|
|
assert registry.get_item_def("cyberdeck_the_miro_model_3a").secret is True
|
|
|
|
|
|
def test_other_cyberdecks_are_not_marked_secret(registry):
|
|
for def_id in (
|
|
"cyberdeck_biomechanics_mk1",
|
|
"cyberdeck_biomechanics_mk2",
|
|
"cyberdeck_infoinc_mk1",
|
|
"cyberdeck_infoinc_mk2",
|
|
"cyberdeck_infoinc_mk3",
|
|
):
|
|
assert registry.get_item_def(def_id).secret is False
|
|
|
|
|
|
def test_hack_succeeds_via_implant_bonus_alone_when_base_skill_would_otherwise_fail(registry):
|
|
world = make_world_with_locked_door(registry)
|
|
human = make_human(registry) # base_tech_skill = 0, would never clear DC 12 unaided
|
|
human.install_implant("cyberdeck_the_miro_model_3a", registry)
|
|
|
|
assert try_hack_tile_layer(human, world, registry, "field", 2, 2, 0, "room", rng=FixedRng(roll=1)) is True
|
|
|
|
tile = world.maps["field"].get_tile(2, 2, 0)
|
|
assert tile.room.def_id == "door_open"
|