automap.combat

The fight: what the machine holds while a battle runs, and where to draw it.

No Qt in here, the same way live.py and render.py have none. The window paints what battlefield yields and the tests assert on it against captured bytes.

Everything is gated on `$6E11 == 2`. LINKER, the 136-byte resident at $2B80, is the outer loop: read $6E11, load that overlay at $0800, call it, repeat – 0 GEN, 1 DUNGEON, `2` COMBAT, 3 INIT, 4 COM.PREP, 5 POST.COM, 8 FINAL, 9 CAMP. The gate is not an optimisation: outside combat $8B00 is a graphics buffer, and in a captured world snapshot it reads 00 00 FF FF ..., so an ungated reader draws combatants stacked at (0,0).

The shape is read, never assumed. SQRPACI<nn> supplies a parameter block at $0600 and the two combat files seen do not agree: SQRPACI01 has a row bounds 55 x 25, SQRPACI00 bounds 17 x 35 – and 18 x 36 squares is exactly the 648 bytes that sit in front of the glyph table in a SQRDATA file. COM.PREP $08C6 derives the camera clamps from the same bytes, which is what proves the reading. The write-up, work/reports/combat-terrain.md, is lost.

Functions

battlefield(battle[, box, cell, margin, shading])

Every primitive for one fight: ground, then combatants.

cell_for(width)

How big a square can be and still fit the window.

extent(battle[, pad, least])

The part of the map worth drawing, as (x, y, width, height).

helpless_indices(save_head)

Which of HELPLESS_TRAITS is on which combatant right now, by index.

read_battle(target[, previous])

The fight in progress, or None when there is not one.

shape_from_params(block)

The shape, or None if these bytes cannot be a parameter block.

square_at(px, py, box, cell[, margin])

Which square a point in the canvas is over.

Classes

Battle

One reading of a fight in progress.

Combatant

One entry of the position table, with its roster block and record.

Shape

The map's geometry, as the $0600 block gives it.

class automap.combat.Battle[source]

Bases: object

One reading of a fight in progress.

__init__(shape, terrain, combatants, camera)
Parameters:
Return type:

None

at(x, y)[source]
Parameters:
Return type:

automap.combat.Combatant | None

camera: tuple[int, int]
combatants: tuple[automap.combat.Combatant, ...]
property enemies: tuple[automap.combat.Combatant, ...]
occupied(x, y)[source]
Parameters:
Return type:

bool

property party: tuple[automap.combat.Combatant, ...]
property round_over: bool

Every initiative byte spent. Nobody left who may still act.

shape: automap.combat.Shape
square(x, y)[source]

The terrain code, with the occupancy bit masked off.

$C086 BPL branches past the glyph lookup when bit 7 is set and draws a combatant instead, so bit 7 is occupancy and bits 0-6 are the ground.

Parameters:
Return type:

int

terrain: bytes
class automap.combat.Combatant[source]

Bases: object

One entry of the position table, with its roster block and record.

record is None when the slot it names holds nothing readable, which is an ordinary state mid-load rather than an error. Everything the tooltip shows is optional for the same reason: a field we have not read is left out.

__init__(index, x, y, slot, pose, on_map, initiative, hp=None, hp_max=None, armour_class=None, thac0=None, movement=None, record=None, helpless=frozenset({}))
Parameters:
Return type:

None

property alive: bool
armour_class: int | None = None
property dimmed: bool

Dead, or gone from the map. Drawn faint rather than removed, so you can see what happened.

helpless: frozenset[int] = frozenset({})

Which of HELPLESS_TRAITS (31, 52, 53) is on this combatant’s index right now, empty if none. Recomputed from the $4900 arrays every poll and never carried forward, so it goes the moment the game clears the id.

hp: int | None = None
hp_max: int | None = None
property hp_text: str
index: int
initiative: int
property is_party: bool
property kind: str

the party green, an enemy red, and a helpless enemy yellow.

The party keeps its green when it is helpless. The fill says which side a square is on before it says anything else, and a yellow party square would read as a third side; the tooltip carries the condition for both.

Type:

How the square is filled

lines()[source]

The tooltip, as text. Only what is decoded.

A field we cannot read is missing from the list rather than guessed at, and a trait code with no name shows its number so that an unnamed code is visibly unnamed.

Return type:

list[str]

movement: int | None = None
property name: str
on_map: bool
pose: int
record: goldbox.record.CharacterRecord | None = None
slot: int
property square: tuple[int, int]
thac0: int | None = None
x: int
y: int
class automap.combat.Shape[source]

Bases: object

The map’s geometry, as the $0600 block gives it.

__init__(map_base, stride, width, height, positions, count)
Parameters:
Return type:

None

count: int
height: int
holds(x, y)[source]
Parameters:
Return type:

bool

index(x, y)[source]
Parameters:
Return type:

int

property length: int

rows are stride apart, so the last one ends there.

Type:

Bytes to read

map_base: int
positions: int
stride: int
width: int
automap.combat.battlefield(battle, box=None, cell=None, margin=MARGIN, shading=SHADING)[source]

Every primitive for one fight: ground, then combatants.

Terrain is drawn as wall or not wall and nothing finer. The glyphs at $91B0 say what each code looks like on the C64’s own screen, but nothing here has been checked against them, and a map that invented a diagonal would be worse than one that draws a block.

Parameters:
automap.combat.cell_for(width)[source]

How big a square can be and still fit the window.

Parameters:

width (int)

Return type:

int

automap.combat.extent(battle, pad=PAD, least=LEAST)[source]

The part of the map worth drawing, as (x, y, width, height).

Both maps seen are 56 x 26 with the fight in a corner, so drawing all 1456 squares would spend the whole window on empty ground. The box covers every combatant and the 7 x 7 window the game itself is showing, padded, and is never smaller than least squares a side.

Parameters:
Return type:

tuple[int, int, int, int]

automap.combat.helpless_indices(save_head)[source]

Which of HELPLESS_TRAITS is on which combatant right now, by index.

Read out of the four $4900 arrays every poll and never remembered: the game clears the id when the condition ends (docs/133-active-effects.md), and a set carried forward would keep a monster gold after it woke up.

The owner byte is the combat combatant index – 0-7 the party in save-slot order, 8 upward the monsters, $FF the whole party – which is why this can say which GOBLIN GUARD is helpless where the shared record cannot. $FF is dropped rather than trusted: no index reaches it, and a party-wide helplessness is not a thing any spell does.

Parameters:

save_head (bytes)

Return type:

dict[int, frozenset[int]]

automap.combat.read_battle(target, previous=None)[source]

The fight in progress, or None when there is not one.

Two bursts, because the map’s address and length are in the first one: the mode byte, the parameter block and the camera, then the map, the roster, the positions, the initiative bytes and the head of the save image, which carries the effect arrays and the twelve record slots together. The cost of a read is the round trip and not the bytes – ~14.3 ms either way under VICE – so the number that matters is two.

previous is last poll’s battle, and supplies the last known square of a combatant that has left the map.

Parameters:

previous (automap.combat.Battle | None)

Return type:

automap.combat.Battle | None

automap.combat.shape_from_params(block)[source]

The shape, or None if these bytes cannot be a parameter block.

Validate before trust: $0600 is ordinary RAM and holds something else entirely between fights – in a captured world snapshot it reads 00 08 00 00 00 8c ..., whose map base is zero.

Parameters:

block (bytes)

Return type:

automap.combat.Shape | None

automap.combat.square_at(px, py, box, cell, margin=MARGIN)[source]

Which square a point in the canvas is over. For the tooltip.

Parameters:
Return type:

tuple[int, int] | None