fix(rules): give Tiny-or-smaller creatures zero reach and square sharing
This commit is contained in:
@@ -657,8 +657,9 @@ Règles modélisées :
|
||||
inchangée. `default_policy` ne choisit pas d'arme (disponible via une
|
||||
politique personnalisée).
|
||||
- Mêlée (conformance R1, CRB « Big and Little Creatures in Combat ») :
|
||||
l'**allonge naturelle** du gabarit (Small/Medium 5 ft, Large 10 ft, Huge
|
||||
15 ft, Gargantuan 20 ft, Colossal 30 ft) est un **disque** : la créature
|
||||
l'**allonge naturelle** du gabarit (Tiny/Diminutive/Fine 0 ft, Small/Medium
|
||||
5 ft, Large 10 ft, Huge 15 ft, Gargantuan 20 ft, Colossal 30 ft) est un
|
||||
**disque** : la créature
|
||||
frappe et menace toutes les cases jusqu'à son allonge, l'adjacence
|
||||
comprise — une créature Large dont l'attaque naturelle a `reach_ft` 10
|
||||
menace donc aussi les cases adjacentes (le `reach_ft` d'une attaque
|
||||
@@ -669,6 +670,19 @@ Règles modélisées :
|
||||
allonge naturelle`, la dead zone vaut donc 5 ft pour un porteur
|
||||
Small/Medium et 10 ft pour un porteur Large. Une arme ordinaire sans
|
||||
allonge menace les cases adjacentes (disque de 5 ft).
|
||||
**S4c — allonge nulle des créatures Tiny et plus petites** (CRB « Big and
|
||||
Little Creatures in Combat » : « Tiny, Diminutive, and Fine Creatures [...]
|
||||
They must enter an opponent's square to attack in melee ») : l'allonge
|
||||
naturelle de Tiny/Diminutive/Fine vaut **0 ft** — une telle créature ne
|
||||
**menace aucune case** (aucune AoO) et ne peut pas frapper une case
|
||||
adjacente ; son attaque de mêlée n'est utilisable qu'à **distance 0**, la
|
||||
case qu'elle partage avec sa cible (l'abstraction « entrer dans la case de
|
||||
l'adversaire »). L'allonge effective reste `max(allonge naturelle,
|
||||
reach_ft de l'arme)` : une attaque naturelle dont le JSON déclare
|
||||
explicitement `reach_ft` (ierlieth : morsure `reach_ft: 5`) conserve cette
|
||||
allonge — le `reach_ft` d'une attaque naturelle EST son allonge naturelle,
|
||||
une créature Tiny peut donc être encodée avec une allonge non nulle si sa
|
||||
fiche l'exige.
|
||||
Mouvement : un déplacement parcourt jusqu'à la
|
||||
vitesse du combattant en cases le long du plus court chemin réel (champ de
|
||||
coût Dijkstra depuis la cible, diagonales 5-10-5), en s'arrêtant adjacent à
|
||||
@@ -711,6 +725,21 @@ Règles modélisées :
|
||||
manœuvres et invocation sont généralisés aux cellules occupées (distance =
|
||||
minimum entre cellules, LoS/couvert = meilleure paire) ; le comportement 1×1
|
||||
reste bit-identique.
|
||||
**S4c — co-occupation des créatures Tiny et plus petites** (CRB « Big and
|
||||
Little Creatures in Combat » : « Very small creatures take up less than 1
|
||||
square of space. This means that more than one such creature can fit into a
|
||||
single square ») : une créature Tiny/Diminutive/Fine peut **traverser et
|
||||
finir son déplacement** dans une case occupée par une créature plus grande,
|
||||
et une créature plus grande peut traverser et finir dans une case occupée
|
||||
par une créature Tiny ou plus petite — la case n'est pas « bloquée » pour
|
||||
ce couple de tailles (`_can_cooccupy` : tailles différentes ET la plus
|
||||
petite est Tiny ou moins). Deux créatures de même taille (ou Small et plus
|
||||
grande) ne partagent jamais une case : le blocage reste total pour elles.
|
||||
La co-occupation s'applique au mouvement (`_blocked`/`_blocked_hostile`
|
||||
sont conscients du déplaceur), à la destination explicite (`_cells_free`)
|
||||
et à la charge ; deux créatures partageant une case sont à **distance 0**
|
||||
l'une de l'autre (ligne d'effet directe, attaque de mêlée possible — c'est
|
||||
la voie d'attaque des créatures à allonge nulle, voir « Mêlée »).
|
||||
- Taille et attaques/CA : les valeurs `ac` (total/touch/flat_footed) et
|
||||
`attack_bonus` des JSON de créatures sont des valeurs FINALES, déjà majorées
|
||||
du modificateur de taille PF1e aux attaques/CA (Small +1, Medium 0, Large −1,
|
||||
@@ -1227,10 +1256,12 @@ Règles modélisées :
|
||||
pénalisé (un test de compétence n'est pas un jet de manœuvre). Conformance
|
||||
L1 (SRD « Combat Maneuvers ») : « Creatures that are size Tiny or smaller
|
||||
use their Dexterity modifier in place of their Strength modifier to
|
||||
determine their CMB » — Tiny/Diminutive/Fine calculent donc leur CMB avec
|
||||
le modificateur de Dextérité à la place de celui de Force ; lacune
|
||||
documentée : l'allonge nulle et la co-occupation des créatures Tiny et
|
||||
plus petites ne sont pas modélisées (abstraction à la grille de 5 ft).
|
||||
determine their CMB » — Tiny/Diminutive/Fine calculent donc leur CMB avec
|
||||
le modificateur de Dextérité à la place de celui de Force. Leur allonge
|
||||
naturelle nulle (aucune case menacée, attaque dans leur propre case) et la
|
||||
co-occupation Tiny-or-smaller avec une créature strictement plus grande
|
||||
sont modélisées (conformance S4c) ; une attaque naturelle dotée d'un
|
||||
`reach_ft` explicite (ex. morsure d'ierlieth) conserve cette allonge.
|
||||
Conformance L2 (SRD « Combat Maneuver Defense ») : le CMD calculé ajoute
|
||||
les bonus de CA des huit types autorisés (« A creature can also add any
|
||||
circumstance, deflection, dodge, insight, luck, morale, profane, and
|
||||
@@ -1391,7 +1422,10 @@ Règles modélisées :
|
||||
- Par combattant : id, side, pos, occupied_cells, hp/hp_max, active/dead,
|
||||
dying/stabilized, conditions (noms + rounds_left), resources_left (+ `spell_slot_{level}`),
|
||||
weapon_broken/weapon_stolen, grappled_with, moved_this_turn, power_attack,
|
||||
name, level, size, aware (SRD « Surprise » : agit au round de surprise),
|
||||
name, level, size, natural_reach_ft (S4c : allonge naturelle du gabarit
|
||||
en pieds — 0 pour Tiny/Diminutive/Fine, 5 pour Small/Medium, 10 pour
|
||||
Large… ; l'allonge effective d'une attaque reste `max(allonge naturelle,
|
||||
reach_ft de l'arme)`), aware (SRD « Surprise » : agit au round de surprise),
|
||||
dr {amount, bypass} ou None (C1), creature_type (C3),
|
||||
bab (E1), initiative_mod (E2) et initiative_total (U2 : total du jet
|
||||
d'initiative courant = d20 + mod, 0 si pas encore de clé — invocation
|
||||
@@ -1514,10 +1548,13 @@ plutôt que bricolées) :
|
||||
spell automatically fails ») : non planifiables dans un tour, ils sont
|
||||
jetés du budget (`downgraded:cast_time`) plutôt qu'approximés, faute de
|
||||
données d'objectif pour ces sorts rituels.
|
||||
- **Gabarits Tiny/Diminutive/Fine abstraits à la grille de 5 ft** : leur
|
||||
allonge nulle et la co-occupation de leurs cases (plusieurs créatures
|
||||
minuscules sur une même case) ne sont pas modélisées ; seule la substitution
|
||||
Dextérité/Force du CMB (conformance L1) tient compte de leur taille.
|
||||
- **Co-occupation au-delà de Tiny vs plus grand** : la règle Tiny-or-smaller
|
||||
est modélisée (S4c) — allonge naturelle 0, aucune case menacée, attaque
|
||||
possible dans sa propre case, et partage de case avec une créature
|
||||
strictement plus grande (déplacement à travers et fin de mouvement dessus) ;
|
||||
en revanche deux créatures de même taille (Small et plus) ne partagent
|
||||
jamais une case, et la co-occupation de plusieurs Tiny sur une même case
|
||||
n'est pas modélisée.
|
||||
|
||||
Limites de sorts (conformance D6-D9, documentées plutôt que bricolées) :
|
||||
|
||||
|
||||
@@ -306,8 +306,15 @@ def _size_rank(size: str) -> int:
|
||||
return _SIZE_ORDER.index("medium")
|
||||
|
||||
# PF1e natural reach for tall creatures (simplification: no long/quadruped
|
||||
# distinction). Unknown sizes fall back to 5 ft (the historical behavior).
|
||||
# distinction). Tiny, Diminutive and Fine have a natural reach of 0 ft (SRD
|
||||
# "Big and Little Creatures in Combat"): "Creatures that take up less than 1
|
||||
# square of space typically have a natural reach of 0 feet, meaning they can't
|
||||
# reach into adjacent squares." Unknown sizes fall back to 5 ft (the historical
|
||||
# behavior).
|
||||
_SIZE_NATURAL_REACH_FT: dict[str, int] = {
|
||||
"Fine": 0,
|
||||
"Diminutive": 0,
|
||||
"Tiny": 0,
|
||||
"Small": 5,
|
||||
"Medium": 5,
|
||||
"Large": 10,
|
||||
@@ -316,6 +323,28 @@ _SIZE_NATURAL_REACH_FT: dict[str, int] = {
|
||||
"Colossal": 30,
|
||||
}
|
||||
|
||||
# PF1e co-occupancy (SRD "Big and Little Creatures in Combat"): "Very small
|
||||
# creatures take up less than 1 square of space. This means that more than one
|
||||
# such creature can fit into a single square" and "They must enter an
|
||||
# opponent's square to attack in melee" — a Tiny-or-smaller creature shares a
|
||||
# square with a strictly larger one (and the larger one may move through and
|
||||
# end in the smaller's square). Small and larger creatures occupy a full
|
||||
# square and never share. The rank ceiling for "takes up less than 1 square".
|
||||
_TINY_OR_SMALLER_MAX_RANK: int = _size_rank("Tiny")
|
||||
|
||||
|
||||
def _can_cooccupy(size_a: str, size_b: str) -> bool:
|
||||
"""True when two sizes may share a single grid square (PF1e co-occupancy).
|
||||
|
||||
Only a Tiny-or-smaller creature ("less than 1 square") and a STRICTLY
|
||||
larger creature share; two same-size creatures never do.
|
||||
"""
|
||||
rank_a = _size_rank(size_a)
|
||||
rank_b = _size_rank(size_b)
|
||||
if rank_a == rank_b:
|
||||
return False
|
||||
return min(rank_a, rank_b) <= _TINY_OR_SMALLER_MAX_RANK
|
||||
|
||||
# Number of grid cells per side of a creature's footprint (square footprint).
|
||||
# Unknown sizes fall back to 1 (the historical single-cell behavior).
|
||||
SIZE_CELLS: dict[str, int] = {
|
||||
@@ -1378,6 +1407,7 @@ class CombatEngine:
|
||||
"name": state.combatant.name,
|
||||
"level": state.combatant.level,
|
||||
"size": state.combatant.size,
|
||||
"natural_reach_ft": _SIZE_NATURAL_REACH_FT.get(state.combatant.size, 5),
|
||||
"aware": state.combatant.aware,
|
||||
"creature_type": state.combatant.creature_type,
|
||||
"bab": state.combatant.bab,
|
||||
@@ -2279,25 +2309,44 @@ class CombatEngine:
|
||||
)
|
||||
|
||||
def _blocked(self, *exclude: CombatantState) -> frozenset[Pos]:
|
||||
"""Union of occupied cells of all active states except those in ``exclude``."""
|
||||
return frozenset(
|
||||
cell
|
||||
for s in self._states
|
||||
if s.active and all(s is not e for e in exclude)
|
||||
for cell in s.occupied_cells()
|
||||
)
|
||||
"""Occupied cells that block the mover (the first excluded state).
|
||||
|
||||
Union of the active creatures' cells except those in ``exclude``. PF1e
|
||||
co-occupancy (SRD "Big and Little Creatures in Combat") lets a
|
||||
Tiny-or-smaller creature share a square with a strictly larger one, so
|
||||
a co-occupiable creature's cells do NOT block the mover (it may move
|
||||
through and end on them); two same-size creatures (or two Small-or-
|
||||
larger) never share, so their cells still block.
|
||||
"""
|
||||
mover = exclude[0] if exclude else None
|
||||
blocked: set[Pos] = set()
|
||||
for s in self._states:
|
||||
if not s.active or any(s is e for e in exclude):
|
||||
continue
|
||||
if mover is not None and _can_cooccupy(
|
||||
mover.combatant.size, s.combatant.size
|
||||
):
|
||||
continue
|
||||
blocked |= set(s.occupied_cells())
|
||||
return frozenset(blocked)
|
||||
|
||||
def _blocked_hostile(self, mover: CombatantState) -> frozenset[Pos]:
|
||||
"""Occupied cells of active creatures hostile to ``mover``.
|
||||
|
||||
Same-side allies are excluded: PF1e (SRD Movement, Position and
|
||||
Distance) — "You can move through a square occupied by a friendly
|
||||
character, unless you are charging."
|
||||
character, unless you are charging." Co-occupiable creatures (a
|
||||
Tiny-or-smaller mover and a strictly larger occupant, or the reverse)
|
||||
are excluded too — the mover may pass through and end on their squares
|
||||
(SRD "Big and Little Creatures in Combat").
|
||||
"""
|
||||
return frozenset(
|
||||
cell
|
||||
for s in self._states
|
||||
if s.active and s is not mover and s.side != mover.side
|
||||
if s.active
|
||||
and s is not mover
|
||||
and s.side != mover.side
|
||||
and not _can_cooccupy(mover.combatant.size, s.combatant.size)
|
||||
for cell in s.occupied_cells()
|
||||
)
|
||||
|
||||
|
||||
@@ -403,3 +403,119 @@ def test_no_aoo_from_adjacent_to_reach_weapon() -> None:
|
||||
"round 1 dest: wait",
|
||||
"battle over: draw after 1 rounds",
|
||||
)
|
||||
|
||||
|
||||
# ── S4c: Tiny/Diminutive/Fine natural reach 0 and co-occupancy ───────────────
|
||||
|
||||
|
||||
def test_tiny_reach_zero_threatens_nothing() -> None:
|
||||
"""S4c: a Tiny creature's natural reach is 0 — it threatens no squares
|
||||
(no AoO) and cannot strike adjacent squares."""
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
state = make_state(tiny, "players", (3, 3))
|
||||
engine = make_engine([state])
|
||||
assert engine.threatened_squares(state) == frozenset()
|
||||
assert (3, 2) not in engine.threatened_squares(state)
|
||||
|
||||
|
||||
def test_tiny_reach_zero_attacks_own_cell_only() -> None:
|
||||
"""S4c: a 0-reach Tiny can attack a creature in its OWN cell (distance 0)
|
||||
but not an adjacent one (distance 1)."""
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
target = make_combatant("target", hp=20, ac=12)
|
||||
tiny_state = make_state(tiny, "players", (3, 3))
|
||||
# Same cell: distance 0 -> the 0-reach attack is usable.
|
||||
foe_same = make_state(target, "monsters", (3, 3))
|
||||
engine_same = make_engine([tiny_state, foe_same])
|
||||
assert engine_same.weapon_for(tiny_state, foe_same) is not None
|
||||
# Adjacent cell: distance 1 (5 ft) -> out of the 0-reach zone.
|
||||
foe_adj = make_state(target, "monsters", (3, 4))
|
||||
engine_adj = make_engine([tiny_state, foe_adj])
|
||||
assert engine_adj.weapon_for(tiny_state, foe_adj) is None
|
||||
|
||||
|
||||
def _tiny_attack_policy(engine: CombatEngine, state: CombatantState) -> tuple[Action, ...]:
|
||||
if state.combatant.id == "tiny":
|
||||
return (Action(kind="attack", target_id="target"),)
|
||||
return (Action(kind="wait"),)
|
||||
|
||||
|
||||
def test_tiny_reach_zero_hits_creature_in_own_cell() -> None:
|
||||
"""S4c: a 0-reach Tiny strikes a creature sharing its cell (the 'entered
|
||||
the square' abstraction) — the attack resolves at distance 0."""
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
target = make_combatant("target", hp=20, ac=12)
|
||||
tiny_state = make_state(tiny, "players", (3, 3))
|
||||
target_state = make_state(target, "monsters", (3, 3))
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([20, 1, 15, 3]),
|
||||
make_grid(), [tiny_state, target_state],
|
||||
round_cap=1, policy=_tiny_attack_policy,
|
||||
)
|
||||
result = engine.run()
|
||||
assert any(
|
||||
"short sword vs target" in line and "-> HIT" in line
|
||||
for line in result.transcript
|
||||
), result.transcript
|
||||
assert target_state.hp == 17 # 20 - 1d6(3)
|
||||
|
||||
|
||||
def _tiny_move_policy(engine: CombatEngine, state: CombatantState) -> tuple[Action, ...]:
|
||||
if state.combatant.id == "tiny":
|
||||
return (Action(kind="move", destination=(3, 3)),)
|
||||
return (Action(kind="wait"),)
|
||||
|
||||
|
||||
def test_tiny_ends_move_in_medium_cell() -> None:
|
||||
"""S4c: a Tiny creature may END its move in a cell occupied by a larger
|
||||
creature (co-occupancy)."""
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
medium = make_combatant("medium", hp=20, ac=15, speed=0)
|
||||
tiny_state = make_state(tiny, "players", (3, 2))
|
||||
medium_state = make_state(medium, "monsters", (3, 3))
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([20, 1, 5]),
|
||||
make_grid(), [tiny_state, medium_state],
|
||||
round_cap=1, policy=_tiny_move_policy,
|
||||
)
|
||||
result = engine.run()
|
||||
assert any("move -> (3,3)" in line for line in result.transcript), result.transcript
|
||||
assert tiny_state.pos == (3, 3)
|
||||
|
||||
|
||||
def _medium_move_policy(engine: CombatEngine, state: CombatantState) -> tuple[Action, ...]:
|
||||
if state.combatant.id == "a":
|
||||
return (Action(kind="move", destination=(3, 3)),)
|
||||
return (Action(kind="wait"),)
|
||||
|
||||
|
||||
def test_two_medium_cannot_share_cell() -> None:
|
||||
"""S4c: two Medium creatures still cannot share a cell — the move onto
|
||||
the occupied cell is rejected."""
|
||||
a = make_combatant("a", speed=30)
|
||||
b = make_combatant("b", hp=20, ac=15, speed=0)
|
||||
a_state = make_state(a, "players", (3, 2))
|
||||
b_state = make_state(b, "monsters", (3, 3))
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([20, 1]),
|
||||
make_grid(), [a_state, b_state],
|
||||
round_cap=1, policy=_medium_move_policy,
|
||||
)
|
||||
result = engine.run()
|
||||
assert any(
|
||||
"move rejected (unreachable_destination)" in line for line in result.transcript
|
||||
), result.transcript
|
||||
assert a_state.pos == (3, 2)
|
||||
|
||||
|
||||
def test_observation_exposes_natural_reach_ft() -> None:
|
||||
"""S4c: the combatant snapshot exposes the size's natural reach."""
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
medium = make_combatant("medium")
|
||||
tiny_state = make_state(tiny, "players", (3, 3))
|
||||
medium_state = make_state(medium, "monsters", (3, 5))
|
||||
engine = make_engine([tiny_state, medium_state])
|
||||
obs = engine._build_observation(tiny_state).to_dict()
|
||||
combatants = {c["id"]: c for c in obs["combatants"]}
|
||||
assert combatants["tiny"]["natural_reach_ft"] == 0
|
||||
assert combatants["medium"]["natural_reach_ft"] == 5
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Tests for Tiny-or-smaller reach zero and square sharing (PF1e S4c).
|
||||
|
||||
SRD "Big and Little Creatures in Combat": Tiny, Diminutive and Fine creatures
|
||||
"take up less than 1 square of space", have "a natural reach of 0 feet", do not
|
||||
threaten the squares around them, and "must enter an opponent's square to
|
||||
attack" — so a Tiny-or-smaller creature and a strictly larger one may share a
|
||||
square (move through and end on it), while two creatures of the same (Small+)
|
||||
size never share.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pf1e_simulator.combat import _effective_reach_ft
|
||||
from tests.test_creature_sizes import make_combatant, make_engine, make_state
|
||||
|
||||
|
||||
class TestTinyReachZero:
|
||||
"""A Tiny creature whose melee attack has reach_ft 0 reaches only its cell."""
|
||||
|
||||
def test_threatens_no_squares(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng.threatened_squares(s_tiny) == frozenset()
|
||||
|
||||
def test_attacks_a_creature_in_its_own_cell(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 3)) # sharing the square
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng.weapon_for(s_tiny, s_foe) is not None
|
||||
|
||||
def test_cannot_reach_an_adjacent_creature(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng.weapon_for(s_tiny, s_foe) is None
|
||||
|
||||
def test_authored_reach_is_kept(self) -> None:
|
||||
# A natural attack authored with reach_ft 5 (e.g. ierlieth's bite)
|
||||
# overrides the size default: the creature reaches and threatens 5 ft.
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=5)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert _effective_reach_ft(s_tiny, tiny.attacks[0]) == 5
|
||||
assert (3, 4) in eng.threatened_squares(s_tiny)
|
||||
assert eng.weapon_for(s_tiny, s_foe) is not None
|
||||
|
||||
|
||||
class TestCoOccupancy:
|
||||
"""A Tiny-or-smaller creature shares a square with a strictly larger one."""
|
||||
|
||||
def test_tiny_may_end_on_a_larger_creature_cell(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng._destination_reachable(s_tiny, (3, 4)) is True
|
||||
|
||||
def test_larger_may_end_on_a_tiny_cell(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 4))
|
||||
s_foe = make_state(foe, "b", (3, 3))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng._destination_reachable(s_foe, (3, 4)) is True
|
||||
|
||||
def test_two_medium_never_share(self) -> None:
|
||||
m1 = make_combatant("m1", size="Medium")
|
||||
m2 = make_combatant("m2", size="Medium")
|
||||
s1 = make_state(m1, "a", (3, 3))
|
||||
s2 = make_state(m2, "b", (3, 4))
|
||||
eng = make_engine([s1, s2])
|
||||
assert eng._destination_reachable(s1, (3, 4)) is False
|
||||
assert eng._cells_free(s2.occupied_cells(), s1) is False
|
||||
|
||||
def test_tiny_may_cross_a_larger_hostile_cell(self) -> None:
|
||||
tiny = make_combatant("tiny", size="Tiny", reach_ft=0)
|
||||
foe = make_combatant("foe", size="Medium")
|
||||
s_tiny = make_state(tiny, "a", (3, 3))
|
||||
s_foe = make_state(foe, "b", (3, 4))
|
||||
eng = make_engine([s_tiny, s_foe])
|
||||
assert eng._cells_walkable(s_foe.occupied_cells(), s_tiny) is True
|
||||
|
||||
def test_medium_cannot_cross_a_medium_hostile_cell(self) -> None:
|
||||
m1 = make_combatant("m1", size="Medium")
|
||||
m2 = make_combatant("m2", size="Medium")
|
||||
s1 = make_state(m1, "a", (3, 3))
|
||||
s2 = make_state(m2, "b", (3, 4))
|
||||
eng = make_engine([s1, s2])
|
||||
assert eng._cells_walkable(s2.occupied_cells(), s1) is False
|
||||
Reference in New Issue
Block a user