User Tools

Site Tools


sd:rogueima_plan

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
sd:rogueima_plan [2026/09/07 14:59] – created - external edit 127.0.0.1sd:rogueima_plan [2026/09/09 03:27] (current) – external edit 127.0.0.1
Line 10: Line 10:
 ===== Part I — movement and the turn ===== ===== Part I — movement and the turn =====
  
-==== Step 1. Collapse movement into one routine ====+==== Step 1. Collapse movement into one routine — DONE ====
  
 ''move_left'', ''move_down'', ''move_up'' and ''move_right'' are four ''move_left'', ''move_down'', ''move_up'' and ''move_right'' are four
Line 16: Line 16:
 ''is_walkable'' call and its own store. Replace them with a single ''is_walkable'' call and its own store. Replace them with a single
  
-  try_move XL = dx, YL = dy+  try_move CL = dx, DL = dy
  
 that computes the destination, bounds-checks it, asks ''is_walkable'' and that computes the destination, bounds-checks it, asks ''is_walkable'' and
Line 24: Line 24:
 and it removes three copies of a bug surface. and it removes three copies of a bug surface.
  
-==== Step 2. Eight-way movement ====+==== Step 2. Eight-way movement — DONE ====
  
 Add ''Y'' ''U'' ''B'' ''N'' for the diagonals, so the vi keys form the usual Add ''Y'' ''U'' ''B'' ''N'' for the diagonals, so the vi keys form the usual
Line 34: Line 34:
 other roguelike. This is an afternoon. other roguelike. This is an afternoon.
  
-==== Step 3. A real turn loop ====+=== The three direction readers === 
 + 
 +Walking was not the only thing that asked for a direction. ''open_door'' and 
 +''do_talk'' each had their own copy of the key list, so after Step 2 you could 
 +//walk// diagonally but not //open// or //talk// diagonally — the same bug in 
 +two more places, which is exactly what Step 1 was meant to prevent. 
 + 
 +The fix is the Step 1 move again, one level up. Four routines now do the work 
 +for all three prompts: 
 + 
 +  get_dir_key   ; read a key, fold it to upper case 
 +  decode_dir    ; AL -> CL = dx, DL = dy, ELM -> the name, carry = valid 
 +  dir_target    ; player + CL/DL -> X, Y, carry = on the map 
 +  print_dir     ; "  OPEN " + "NORTHEAST" + newline, preserving C and D 
 + 
 +''decode_dir'' is now the only place in the game that knows what a movement 
 +key is. Adding a fifth prompt that wants a direction — fire, throw, kick — 
 +costs two calls. 
 + 
 +Two details worth keeping in mind: 
 + 
 +  * ''ELM'' and ''FLD'' are //paired// registers (''EL:M'', ''FL:D''), so 
 +    ''FLD'' cannot hold a pointer while ''D'' holds a delta. ''print_dir'' 
 +    takes its prefix in ''GLK'' and consumes it before any call. 
 +  * ''dir_target'' bounds-checks, which ''open_door'' and ''do_talk'' never 
 +    did. Reading a glyph one step off the edge of the map used to read 
 +    whatever followed the map data. 
 + 
 +''"  OPEN DOOR "'' plus ''"NORTHEAST"'' is 21 characters and the message 
 +window is 19 wide, so the prefix lost the word the prompt above it already 
 +supplies. 
 + 
 +==== Step 3. A real turn loop — DONE ====
  
 Today the player moves and the world does not answer. Establish the order: Today the player moves and the world does not answer. Establish the order:
Line 43: Line 75:
 Everything from here assumes monsters get a turn when you take one. Everything from here assumes monsters get a turn when you take one.
  
-==== Step 4. Wait ====+=== The turn contract === 
 + 
 +''move_mon'' was being called from inside ''draw_map'', so monsters moved once 
 +per //redraw//, before the player's key had even been read. It is a call in the 
 +loop now, and the loop is the whole of the ordering: 
 + 
 +  main_loop: 
 +      CALL @draw_map 
 +      CALL @get_input         ; the player acts 
 +      JNC @main_loop          ; ...or did not: redraw and ask again 
 +      CALL @move_mon          ; now the world answers 
 +      CALL @inc_game_time     ; and the clock moves 
 +      JMP @main_loop 
 + 
 +Every command routine answers the same question in the carry flag: 
 + 
 +  ^ carry ^ meaning ^ 
 +  | set   | the player spent a turn | 
 +  | clear | nothing happened | 
 + 
 +''get_input'' tail-jumps to those routines, so their carry //is// its carry and 
 +no dispatch code has to know which commands cost time. Walking into a wall, 
 +naming a direction with no door in it, and pressing an unknown key are all 
 +free — the world does not get to answer a non-move. ''M'' and ''G'' conjure 
 +things out of nothing, so they are free too; they are debug commands. 
 + 
 +=== Three things this uncovered === 
 + 
 +Giving monsters a real turn meant there had to //be// monsters, and there 
 +never had been: 
 + 
 +  * ''init_objects'' said ''"Write id = 0 for this record"'' but the 
 +    instruction was ''STB [ELM + I]'', and ''B'' was never zeroed. Every slot 
 +    was born with a non-zero ID — that is, "in use" — so the ''SCANQUE'' in 
 +    ''spawn_monster'' and ''create_gold'' never found a free one. **''M'' and 
 +    ''G'' had been failing silently.** Whether this bit depended on what the 
 +    title screen happened to leave in ''B''
 +  * ''MOD B, KL'' mixes a 16-bit destination with an 8-bit source. ''MOD'' 
 +    takes its width from the destination, so this read ''REGS[30]'' — past the 
 +    end of the 16-bit register file — and ''B'' came back unreduced. Monsters 
 +    and gold were being placed at coordinates like (56242, 15977), off the map 
 +    and out of reach. Both spawners widen the dimensions into ''I'' and ''J'' 
 +    first now. 
 +  * ''move_mon'' still had a ''SED''/''CLD'' pair bracketing its commit — 
 +    leftover CPU-trace instrumentation, harmless only for as long as no 
 +    monster ever moved. 
 + 
 +=== Machine note === 
 + 
 +The assembler rejects mismatched widths for ''MOV'' but accepts them for 
 +arithmetic, and the CPU then indexes the register file with the raw encoding. 
 +''MOD B, KL'' is not a typo the tools will catch. Worth a width check on the 
 +arithmetic path. 
 + 
 +==== Step 4. Wait — DONE ====
  
-''.'' passes a turnTrivial once Step 3 exists, and it is the only way to +Numpad ''5'' and ''.'' both reach ''move_wait''which is now the one move 
-watch monster behaviour without moving, which makes the next several steps +that is nothing but time: it returns carry set without touching ''PX''/''PY'', 
-testable.+so the monsters get their turn and the clock advances. That makes it the way 
 +to watch monster behaviour without moving, which is what makes the next 
 +several steps testable.
  
 ===== Part II — depth ===== ===== Part II — depth =====
  
-==== Step 5. Turn the map into a structure ====+==== Step 5. Turn the map into a structure — DONE ====
  
 ''map1_data'' is literal text — 40 lines of ''.bytes "####..."''. That is fine ''map1_data'' is literal text — 40 lines of ''.bytes "####..."''. That is fine
Line 64: Line 152:
 need per-tile storage, and doing any of them first means doing them twice. need per-tile storage, and doing any of them first means doing them twice.
  
-==== Step 6. A level table ====+=== What landed === 
 + 
 +Two bytes per square, row by row, at ''$030000'' in bank 3 -- 6,400 bytes for 
 +80x40: 
 + 
 +  tile(x, y) = TILE_BASE + (y * width + x) * 2 
 + 
 +^ flag ^ value ^ meaning ^ 
 +| ''TF_WALKABLE'' | ''$01'' | you can stand here | 
 +| ''TF_OPAQUE''   | ''$02'' | you cannot see through it | 
 +| ''TF_SEEN''     | ''$04'' | reserved for Step 10 | 
 +| ''TF_VISIBLE''  | ''$08'' | reserved for Step 10 | 
 + 
 +''SEEN'' and ''VISIBLE'' are defined now and set on every square, so the 
 +renderer behaves exactly as it did. Installing the machinery separately from 
 +switching it on is what keeps Step 10 small. 
 + 
 +''load_level'' expands the text into the structure on entry; ''tile_flags_for'' 
 +is the single place that decides what a glyph means, so the loader and 
 +''put_glyph'' cannot disagree -- which they would, the first time a door 
 +opened. ''is_walkable'' is a flag test now instead of a chain of glyph 
 +comparisons, and no longer walks the object list to get there. 
 + 
 +The test for this step is that **nothing changes on screen**: the rendered 
 +display is byte-identical before and after, with the camera scrolled. 
 + 
 +=== The assembler bug underneath it === 
 + 
 +''AND AL, @TF_WALKABLE'' is the first 8-bit immediate in the tree naming a 
 +label defined in a //later// file, and forward-reference patching got that 
 +wrong: it wrote the low byte, then wrote a second byte unconditionally, 
 +zeroing the next instruction's opcode. Here that opcode was a ''JZ'', so 
 +''is_walkable'' fell through its own branch and nothing on the map could be 
 +stepped on -- from an image that assembled with no errors and no warnings. 
 + 
 +Worth remembering as a shape: a program that assembles cleanly and behaves 
 +absurdly is worth a look at the emitted bytes, and then at the trace. 
 + 
 +==== Step 6. A level table — DONE ====
  
 ''map1_id'', ''map1_name'', ''map1_up'', ''map1_down'' and ''map1_dim'' already ''map1_id'', ''map1_name'', ''map1_up'', ''map1_down'' and ''map1_dim'' already
Line 72: Line 198:
 entry to begin with. entry to begin with.
  
-==== Step 7. Stairs ====+==== Step 7. Stairs — DONE ====
  
 ''<'' and ''>''. Entering a staircase switches the active level descriptor and ''<'' and ''>''. Entering a staircase switches the active level descriptor and
Line 78: Line 204:
 already in the data. already in the data.
  
-==== Step 8. A second level ====+=== What landed === 
 + 
 +The descriptor, which is the old ''map1_*'' fields plus the two pointers they 
 +were missing: 
 + 
 +^ offset ^ field ^ ^ 
 +| 0  | ''LV_ID''    | 1 byte | 
 +| 1  | ''LV_NAME''  | 9 bytes | 
 +| 10 | ''LV_DIM''   | width, height | 
 +| 12 | ''LV_UP''    | x, y of the staircase up | 
 +| 14 | ''LV_DOWN''  | x, y of the staircase down | 
 +| 16 | ''LV_SRC''   | -> the source text | 
 +| 19 | ''LV_TILES'' | -> this level's tile array | 
 + 
 +Every level is built at startup and keeps its own tile array, so **levels 
 +persist** -- a door you opened is still open when you come back. There is 
 +nothing to gain by discarding one: a 26-level dungeon is under 170 KB. 
 + 
 +''enter_level'' mirrors the dimensions and tile pointer into ''level_dim'' and 
 +''level_tiles'', because ''tile_addr'' and ''draw_world'' read them per square 
 +and per cell. 
 + 
 +Both commands check you are standing on the staircase, switch the descriptor, 
 +and put you on the matching stair of the destination -- down arrives at the 
 +level's //up// stair, and the reverse. Both honour the turn contract: a flight 
 +of stairs is a turn, refusing to move is not. 
 + 
 +''place_stairs'' stamps ''<'' and ''>'' onto each level as it is built. Those 
 +coordinates had been in the data since the game was written and nothing had 
 +ever drawn them. 
 + 
 +The table has two rows. Level 2 borrows level 1's text as a placeholder -- 
 +Step 8 replaces that single pointer. Its //tiles// are already separate, which 
 +is what made the persistence testable: open a door on level 1, and the same 
 +square on level 2 is still shut. 
 + 
 +==== Step 8. A second level — DONE ====
  
 Hand-authored, in the same text format. Proves the machinery of Steps 5-7 Hand-authored, in the same text format. Proves the machinery of Steps 5-7
 before any of it depends on a generator. before any of it depends on a generator.
  
-==== Step 9. DungeonMaker ====+The machinery is already in place and exercised -- what is left is **content**. 
 +One pointer in ''level_table'' row two changes from ''@map1_data'' to 
 +''@map2_data''
 + 
 +=== What landed === 
 + 
 +**BrynnWell**, the well maze from NetWhack'
 +''src/netwhack/world/mapgen/BrynnWell.java'' -- 20 x 30. Exactly one pointer in 
 +the table changed, which is what Steps 6 and 7 were for. 
 + 
 +Glyph for glyph: NetWhack's ''.'' is our '' '', ''#'' and ''+'' carry over, and 
 +its six secret doors (''s'') are ordinary doors here. There is no search 
 +command yet, so a secret door would be a wall you could never get through; they 
 +can go back to being secret once something can find them. 
 + 
 +It is a **20 x 30 level**, not a 20 x 30 maze padded out to match level 1. 
 +That needed two fixes in ''draw_world'', which was the last place still 
 +assuming one map size: 
 + 
 +  * the camera clamp computed ''(map_width - view_width)'', which underflows 
 +    when the map is the narrower of the two. A map that fits has nowhere to 
 +    scroll to, so the answer is zero. 
 +  * the render loop drew 58 x 23 squares unconditionally, so past the right 
 +    edge of a narrow map it carried on into the start of the next row. 
 +    Squares outside the map draw as nothing now. 
 + 
 +Maps //larger// than the view always worked -- that is the scrolling case. 
 +Everything else had carried per-level dimensions since Step 6. Level 1 is 
 +unaffected and provably so: its rendered screen is byte-identical before and 
 +after, walked to the same square with the same door open. 
 + 
 +Verified by rebuilding the level out of its tile array and diffing against the 
 +Java source: all 30 rows match, with ''<'' and ''>'' stamped where the 
 +descriptor says. 
 + 
 +==== Step 9. DungeonMaker — DONE ====
  
 Rooms and corridors, seeded from the RNG so a level is reproducible from its Rooms and corridors, seeded from the RNG so a level is reproducible from its
Line 93: Line 290:
 3,200 bytes, so a 26-level dungeon fits inside a single bank. Levels can simply 3,200 bytes, so a 26-level dungeon fits inside a single bank. Levels can simply
 persist. persist.
 +
 +=== What landed ===
 +
 +''programs/rogueima/dungeon.sda''. Fill the level with rock, mine one room in
 +the middle, then repeatedly find a wall with **exactly one** floor square
 +beside it and build on the far side, leaving a door in the wall you dug
 +through.
 +
 +That one-floor-neighbour rule is doing two jobs. It stops a level collapsing
 +into a single cave, and it is also why every level comes out connected: each
 +new piece is reachable through the door that made it. Connectivity is a
 +property of the construction, not something checked afterwards.
 +
 +Levels 3, 4 and 5 are generated, 78 x 22. ''LV_SRC'' of 0 in the level table
 +means "there is no text to expand, build it", and the generator writes
 +''LV_UP'' and ''LV_DOWN'' into the descriptor itself -- a level that does not
 +exist yet cannot say where its stairs are. Generated levels persist exactly
 +like the hand-made ones.
 +
 +=== The corridors NetWhack never built ===
 +
 +''addfeature'' in ''DungeonMaker.java'' reads:
 +
 +  switch (feature_type) {
 +      case 1:  success = makeshop(...); break;
 +      case 2:
 +      default: success = makeroom(...); break;
 +  }
 +
 +''case 2:'' is an empty label falling straight into ''default:'', so it builds
 +a room like everything else. ''TileData.CORRIDOR'' exists and ''readmapline''
 +can produce one, but nothing ever generated one. The branch was never written
 +rather than broken -- which is why BDungeon levels are rooms hanging off rooms.
 +
 +Writing it costs almost nothing, because **a corridor is a room with one
 +dimension collapsed to zero**, and ''makeroom'''s geometry already handles
 +that: building EAST with height 0 gives ''a.y = b.y = yloc'', a single row. So
 +rooms and corridors are one routine called with different sizes, and the
 +feature roll now actually branches.
 +
 +=== Verification ===
 +
 +Dump the tile array and analyse it rather than looking at it. Level 3 came out
 +310 floor squares with all 310 reachable by flood fill; level 4, 388 of 388.
 +Solid border, 18 and 20 doors, both staircases placed, and the two levels
 +different from each other. Corridor squares -- floor with exactly two opposite
 +floor neighbours -- numbered 70 on level 3.
  
 ===== Part III — sight ===== ===== Part III — sight =====
  
-==== Step 10. Per-tile seen and visible bits ====+==== Step 10. Per-tile seen and visible bits — DONE ====
  
 Add the two flags to the tile structure from Step 5, and teach the renderer Add the two flags to the tile structure from Step 5, and teach the renderer
Line 105: Line 349:
 small and makes a regression obvious. small and makes a regression obvious.
  
-==== Step 11. Line of sight ====+=== What landed === 
 + 
 +The flags have existed since Step 5, set on every square. The renderer reads 
 +them now: 
 + 
 +^ state ^ drawn ^ colour ^ 
 +| ''TF_VISIBLE'' | lit | ''$07'' light grey on black | 
 +| ''TF_SEEN'' only | dim | ''$08'' dark grey on black | 
 +| neither | not at all | -- | 
 + 
 +They are still set on every square, so **nothing changes on screen** -- which 
 +is the point. Step 11 only has to start clearing ''TF_VISIBLE''
 + 
 +Colour arrives with this step, because "dim" needs one. Palette 5, CGA as an 
 +IBM 5153 showed it, and the indices are NetWhack's own: its ''ColorMap'' puts 
 +light grey at 7 and dark grey at 8 in the same order, so a colour there is a 
 +colour here. ''TileData'' also colours by //tile kind// -- doors ''BROWN'', 
 +staircases ''WHITE'' -- which maps onto the same palette and is worth adding. 
 + 
 +Two traps worth writing down: 
 + 
 +  * ''draw_borders'' clears the screen every redraw, and the clear repaints 
 +    the colour plane with the mode's default. The colours have to go back on 
 +    after it, not once at startup. 
 +  * ''print_char'' takes its colour from ''VIDEO_CHAR_COLOR'', not from the 
 +    plane. That is how the status panel is painted, and without setting it the 
 +    panel comes out dark grey on brown under a CGA palette. 
 + 
 +Verified by dumping both planes: glyphs unchanged, colour uniformly ''$07'' 
 +across all 2000 cells. Clearing ''TF_VISIBLE'' on one square by hand drew it 
 +dim with its glyph intact; clearing ''TF_SEEN'' as well drew nothing, leaving 
 +a gap in the wall. 
 + 
 +==== Step 11. Line of sight — DONE ====
  
 Each turn, clear ''visible'', then walk a line from the player to every tile Each turn, clear ''visible'', then walk a line from the player to every tile
Line 117: Line 394:
 list.** A lit corridor ahead and a remembered room behind is the difference list.** A lit corridor ahead and a remembered room behind is the difference
 between a map and a dungeon. between a map and a dungeon.
 +
 +=== What landed ===
 +
 +''programs/rogueima/los.sda''. NetWhack's ''visline()'' unchanged, but cast to
 +a **radius of 8** rather than to the edge of the map.
 +
 +''makevis()'' there casts to every square on the map boundary -- about 200 rays
 +of up to 78 steps, which its own comment calls //"a terrible waste of
 +resources"//. A radius is the same code with nearer endpoints: about 68 rays of
 +at most 11 steps, some thirty times less work, and it gives a torch instead of
 +sight to the far wall. ''LOS_RADIUS'' is one constant.
 +
 +The radius is **circular**: a square is in range only if
 +''dx*dx + dy*dy <= 64'', so the corners of the box do not see further than the
 +sides. Two MULs per square.
 +
 +Bresenham is the error-accumulator form, which keeps every quantity
 +non-negative -- the signs live in ''LOS_SX''/''LOS_SY'' and are applied as
 +8-bit wraparound. No signed comparisons, which on this machine would have meant
 +testing N against V by hand.
 +
 +Switching it on was **one line**: ''TF_FLOOR'' and ''TF_WALL'' stop carrying
 +''SEEN'' and ''VISIBLE'', so a square starts unknown. That is what Step 10 was
 +for.
 +
 +Three things came with it:
 +
 +  * ''put_glyph'' preserves ''SEEN'' and ''VISIBLE''. Opening a door changes
 +    what a square //is//, not whether you have been there.
 +  * a known floor draws as ''.'', because floor is stored as a space and a
 +    blank square is what "never seen" looks like -- lit floor was invisible.
 +  * monsters and items are only drawn where ''TF_VISIBLE'' is set, or you see
 +    them through walls.
 +
 +=== Verification ===
 +
 +Dump the glyph and colour planes together and separate lit from remembered by
 +colour. In the open: a clean circle of lit floor spanning exactly ''px±8'' and
 +''py±8'', with a dim crescent trailing from where the player walked. In
 +BrynnWell's maze, standing in a corridor:
 +
 +  #   #####
 + +....@.+
 +  #   #####
 +
 +the corridor, the walls above and below it, the closed doors at each end, and
 +nothing through them.
  
 ===== Part IV — things to carry ===== ===== Part IV — things to carry =====
  
-==== Step 12. Inventory ====+==== Steps 12–14. Inventory, item classes, wield and wear — DONE ==== 
 + 
 +Planned as three steps and built as one, because none of them is any use 
 +alone: an item you cannot carry, a pack you cannot look in, or a suit you 
 +cannot put on. All of it lives in the new ''item.sda''
 + 
 +=== The item table === 
 + 
 +NetWhack knows what an item is from its **class**: ''Armor'' extends ''Item'', 
 +carries an ''acmod'', and takes its name and numbers from ''ArmorData[kind]''
 +Here the class is the ''OBJ_TYPE'' tag and the per-class payload is 
 +''OBJ_DATA1'' — a tagged union, which is the same shape without the 
 +inheritance. What the tag does //not// give you is the data, and that is the 
 +table: 
 + 
 +  .equ IT_TYPE  0     ; which class this is 
 +  .equ IT_GLYPH 1 
 +  .equ IT_STAT  2     ; the class's one number 
 +  .equ IT_NAME  4     ; -> the name 
 +  .equ IT_SIZE 
 + 
 +  item_table: 
 +      .bytes 3, ')',  3, 0, @str_dagger     ; WEAPON, 1d3 
 +      .bytes 4, '[', 10, 0, @str_leather    ; ARMOR,  acmod 10 
 + 
 +''IT_STAT'' is the one number the class needs — a weapon's damage sides, a 
 +suit's ac modifier — exactly as ''WeaponInfo'' carries ''dmg'' and 
 +''ArmorInfo'' carries ''acmod''. **A new item is a row here, not a branch in 
 +''make_item''.** 
 + 
 +=== The pack === 
 + 
 +A second queue over the //same// fixed node array the world uses. Picking 
 +something up is ''REMQUE'' from the world list and ''INSQUE'' into the pack; 
 +the node itself never moves and nothing is copied. That is what 
 +''INSQUE''/''REMQUE'' are for, and it is why ''draw_items'' stops drawing 
 +something the moment you take it — it walks the world queue, and the node is 
 +no longer on it. 
 + 
 +=== The screen === 
 + 
 +ADOM by way of NetWhack: a full page, grouped by category, one letter per 
 +item. Letters are the item's position in the pack, so an item keeps its letter 
 +whichever heading it appears under. 
 + 
 +    INVENTORY 
 + 
 +    Weapons 
 +      a - dagger (wielded) 
 +    Armour 
 +      b - leather armor (worn) 
 + 
 +    -- press any key -- 
 + 
 +''I'' to look, ''E'' to wield, ''W'' to wear, ''R'' to take everything off, 
 +''D'' to drop, and the existing ''‚'' extended to pick items up. ''X'' is a 
 +debug command that drops one of each at your feet. 
 + 
 +=== What it does to combat === 
 + 
 +''do_combat'' used to land every time and take off exactly 1. Now it rolls 
 +**2d(attack) against 2d(defense)**, as NetWhack does, with ties to the 
 +defender; wielding anything at all is worth +10 attack, armour adds its 
 +''acmod'', and damage is the wielded weapon's die. 
 + 
 +=== Verification === 
 + 
 +A miss proves nothing, so the rolls are called directly rather than played. 
 +''test_combat.sda'' runs a thousand trials each way and leaves four totals in 
 +memory: 
 + 
 +  hit by a monster, unarmoured      458 / 1000 
 +  hit by a monster, in leather      148 / 1000 
 +  damage over 1000 swings, fist    1000        (a fist is always 1) 
 +  damage over 1000 swings, dagger  2006        (1d3, mean 2) 
 + 
 +Armour makes you harder to hit and the weapon changes the damage, which were 
 +the two things this step set out to show. 
 + 
 +=== The memory map, which moved === 
 + 
 +The step also flushed out a real bug. The game sat at ''$02C000'' with the 
 +object array 12 KB above it at ''$02F000''. The code grew past that line, so 
 +''init_objects'' cleared the node array **over the game's own instructions** — 
 +which presents as the player being unable to move, with the assembler 
 +reporting nothing. 
 + 
 +The game now loads at ''$020100'': ''USER_ORIGIN'', the address the shell and 
 +''INT $20'' already load programs to, so an assembled Rogueima is a program the 
 +shell can run by name like any other. Everything it allocates is a whole bank 
 +away, one bank per kind: 
 + 
 +  $020100   bank 2: code and data, growing up   (55 KB to the FS command block) 
 +  $030000   bank 3: game data -- the objects 
 +  $040000   bank 4: the maps -- one tile array per level 
 + 
 +The bases are labels and everything derives from them, so the second map bank 
 +a real dungeon will need is one line. ''.equ'' learned ''@label + N'' to make 
 +that possible. 
 + 
 +Two smaller fixes fell out of testing: ''draw_items'' only ever drew gold, so 
 +a dropped dagger vanished (it now draws every object that is not a monster), 
 +and ''inv_ask'' compared item letters against lowercase ''a'' while 
 +''get_dir_key'' folds keys to upper case, so no item could ever be selected. 
 + 
 +=== Since then: the whole item table === 
 + 
 +Steps 12-14 shipped with two items written by hand. ''itemtable.sda'' is now 
 +NetWhack\'s five item arrays -- ''ArmorData'', ''WeaponData'', ''FoodData'', 
 +''PotionData'', ''ScrollData'', **41 items** -- generated by 
 +''tools/gen_itemtable.py''. Five arrays of five ''Info'' classes with five 
 +different constructor signatures come out as one table with a class tag and a 
 +per-class payload. 
 + 
 +**''IT_SLOTS'' is the field that earned it.** ''ItemInfo.eqslots'' is the //set// 
 +of equipment slots an item may occupy: gloves name ''GLOVES'' and nothing else, 
 +a dagger names ''WEAPON'' and ''OFFHAND''. There was one ''eq_weapon'' and one 
 +''eq_armor'', and between them they cannot express a pair of gloves -- putting 
 +gloves on took your plate off, and the only symptom was a defence number that 
 +went **down** when you added armour. There is an equipment rack now, one 
 +pointer per slot, and ''defense_rating'' sums every worn piece. 
 + 
 +  defence 10 bare  ->  60 in field plate  ->  64 in plate and gloves 
 + 
 +=== The equipment page === 
 + 
 +''Inventory.imEquipment()''. ''w'' opens a page listing all thirteen slots at 
 +once, lettered A to M. Press a slot: something in it comes off, an empty one 
 +offers the things that fit **that slot and nothing else**, which is 
 +''getitem_byslot()''. A single "wear what?" prompt has to guess where a thing 
 +goes and cannot show you what is empty, which is the question a player has. 
 + 
 +=== Identification === 
 + 
 +Only potions and scrolls have a fake name -- ''ArmorInfo'', ''WeaponInfo'' and 
 +''FoodInfo'' take no such argument, because a sword looks like a sword. 
 + 
 +Two things about ''PotionData.randomize()'' that are easy to get wrong. It 
 +swaps the fakename **and the colour** together, so an appearance is a 
 +(name, colour) //pair// -- get it wrong and you have a milky potion drawn in 
 +green. And ''identified'' is a field of the DATA row, not of the object: drink 
 +one potion of healing and every potion of healing is named for the rest of the 
 +game. Neither can live in the generated table, so both are a byte per kind in 
 +the data bank. 
 + 
 +Names are therefore **built**, not stored: "potion of moonshine", or "milky 
 +potion" when the appearance begins with a capital, as ''Potion.name()'' does.
  
-''i'' to list, ''d'' to drop, and extend the existing ''‚'' pickup. Carried +=== Paging ===
-items are a queue, and the machine has ''INSQUE''/''REMQUE''/''SCANQUE'' for +
-exactly this — ''obj.sda'' already uses them for objects lying in the world.+
  
-==== Step 13Item classes ====+''Inventory.getitem_step2/step3'' -- the one screen every "choose something" 
 +goes through, and, since the shops use it too, worth having once. Letters 
 +restart at ''a'' on every page, as NetWhack does: the letter means "the third 
 +thing I can see" and must not depend on how long the list is.
  
-shared header — kind, glyph, name pointer, weight — with a per-class payload +=== bug worth writing down ===
-after it. **Weapons and armour first**, because ''combat.sda'' already exists +
-and will show the difference immediately.+
  
-==== Step 14Wieldweartake off ====+Two commits of new variables were placed on top of existing ones in the 
 +hand-managed variable page''inv_more'' sat on ''item_kind''so the class 
 +filter was overwritten halfway through drawing the inventory and **the weapons 
 +and armour vanished from it**. Fourteen overlaps in allno assembler warning, 
 +and the symptom in a routine that looked innocent.
  
-''w''''W'', ''T''Equipment slots feed the existing combat resolution: +''tools/check_vars.py'' reads the ''.equ'' declarations and their "; N bytes" 
-weapon damage, armour class.+comments and reports overlapsRun it after adding one.
  
-==== Step 15. Consumables and an effects hook ====+==== Step 15. Consumables and an effects hook — HALF DONE ====
  
 Food, potions, scrolls, and a small effect dispatch table they trigger. Food, potions, scrolls, and a small effect dispatch table they trigger.
-NetWhack keeps 12 of each and an ''Effect'' classdozen effects is plenty to + 
-start.+The **things** all exist: importing NetWhack's item tables brought 7 foods, 5 
 +potions and 5 scrolls with their real numbers, and with them the 
 +identification system (see below). ''e'' eats and ''q'' quaffs, and a potion 
 +gives you its nutrition -- which for poison is //negative//
 + 
 +What is left is the **effects**: healing, blindness, speed, teleport, identify, 
 +town portal. NetWhack has an ''Effect'' class with trigger type, and 
 +''Food.event_eat'' already calls ''fxlist.transferByItemTrigger(this, 
 +Effect.T_FOOD)''. Until that exists, a potion of speed is a drink of water 
 +with a different name on it.
  
 ===== Part V — pressure ===== ===== Part V — pressure =====
  
-==== Step 16. Hunger ====+==== Step 16. Hunger, as an energy meter — DONE ====
  
-A food clock, and ''e'' to eat. This is what turns wandering into a game: it is +A food clock, and ''e'' to eat. Taken ahead of Step 15 because it is the one 
-the reason to go down rather than explore forever.+that changes how the game is played: it is the reason to go down rather than 
 +explore forever.
  
-==== Step 17. Regeneration and death ====+=== Inverted ===
  
-HP returning slowly with time, resting to pass turns safely, and a proper death +This is NetWhack's ''Engine.pc_hunger()'' with the sign flipped. There, 
-— tombstone, final score, and a return to the prompt rather than a hang.+''pc.hunger'' counts **up** from zero and every test asks how big it has got: 
 + 
 +  pc.hunger++; 
 +  if (pc.hunger > 5000) { ... } 
 +  if (pc.hunger > 4000) { ... } else if (pc.hunger > 3200) { ... } 
 + 
 +Here the same number counts **down** from ''EN_MAX'' and is called energy. 
 +They are the same meter — every threshold is 5000 minus NetWhack's — but the 
 +down-counting one can go on the status panel and be read without explaining. 
 +//Energy: 800// says you are nearly out of something. //Hunger: 4200// does 
 +not. 
 + 
 +^ NetWhack ^ Rogueima ^ says ^ odds ^ 
 +| hunger > 800  | energy < 4200 | You're feeling a might peckish. | 1 in 500 | 
 +| hunger > 1600 | energy < 3400 | Your stomach is grumbling.      | 1 in 500 | 
 +| hunger > 2400 | energy < 2600 | You are feeling hungry.         | 1 in 500 | 
 +| hunger > 3200 | energy < 1800 | You are feeling very hungry.    | 1 in 400 | 
 +| hunger > 4000 | energy < 1000 | You are starving!               | 1 in 250 | 
 +| hunger > 5000 | energy == 0   | starving.. TO DEATH! (-1 hp)    | 1 in 50  | 
 + 
 +Two consequences of the inversion, both small: 
 + 
 +  * eating **adds and clamps at the top** rather than subtracting toward zero, 
 +    so the bug to avoid is an unsigned overflow rather than an underflow. 
 +    NetWhack lets hunger go negative on a big meal; a meter has a maximum. 
 +  * starving is energy **at** zero rather than hunger past 5000. NetWhack'
 +    hunger keeps climbing past its own limit and nothing reads it up there, so 
 +    nothing is lost by parking at the bottom instead. 
 + 
 +=== The clock === 
 + 
 +NetWhack calls ''pc_hunger'' when ''(gametime % TICKS_PER_TURN) == 0'', because 
 +its ''gametime'' is fine-grained and ticks far faster than the player acts. 
 +Rogueima's ''game_time'' already counts player turns and only advances when one 
 +is spent, so the modulo is the identity here — ''main_loop'' just calls it once 
 +a turn. 
 + 
 +At one point of drain per turn, a full meter is **five thousand turns**. That 
 +is deliberately a minor concern for now: it is there, it works, and the numbers 
 +are the thing to argue about when balance is the concern. 
 + 
 +=== Eating === 
 + 
 +''e'' eats. A ration is ''FoodData[2]'' — 500 nutrition, glyph ''%'' — and it 
 +is a **row in ''item_table''**, not a branch anywhere, which is what Step 13 
 +bought. NetWhack's ''Food.event_eat'' also heals a point when 
 +''Dice.roll(1, nutrition) > 200'', so a ration is a three-in-five chance of one 
 +hp back. 
 + 
 +Disposal is the fiddly part, and it is not a ''REMQUE'' alone. ''make_item'' 
 +finds a free node by scanning the **world** queue for one with a zero id, so an 
 +item taken off the pack queue and put on no queue at all is leaked rather than 
 +freed. Eating therefore puts the node back on the world queue and only then 
 +zeroes the id — the same disposal ''pick_up_gold'' does, from the other end. 
 + 
 +=== Verification === 
 + 
 +Five thousand turns of drain is not something you can sit through, and a 
 +complaint that fires one turn in five hundred is not something you can see by 
 +playing. ''test_hunger.sda'' calls ''pc_hunger'' directly with the meter 
 +**pinned** at a chosen level, which holds it inside one band of the chain, and 
 +asks whether that band says anything — by clearing the message buffer first and 
 +counting the non-zero bytes after. 
 + 
 +  drain past the bottom                  0        (floors, does not wrap) 
 +  hp lost at empty over 5000 turns       105      (1 in 50, so about 100) 
 +  messages at energy 4300                0 bytes  (above every band: silent) 
 +  messages at 4100/3300/2500/1700/900    247 bytes each 
 + 
 +The silent row is the one that matters as much as the loud ones: it pins the 
 +''EN_PECKISH'' boundary from above, so an inverted comparison could not pass. 
 + 
 +And in the real loop, poked down to 30 and left to walk: energy floored at 0 
 +and hp went 10 → 6 while starving. 
 + 
 +=== A bug it turned up === 
 + 
 +''pick_up_items'' listed the types that //could// be carried, so the ration was 
 +un-pickable the day it was added. That is the same shape as ''draw_items'' only 
 +ever drawing gold, found in Step 12. Both now name the one type they **exclude** 
 +— a new kind of item should not need a line in either place. 
 + 
 +==== Step 17. Regeneration and death — HALF DONE ==== 
 + 
 +**Regeneration is in.** ''Mobile.per_turn()''\'s healing half: one turn in a 
 +hundred you mend a point, and NetWhack\'s ''hunger++'' beside it becomes a 
 +second point off the energy meter. That is the link that makes food matter for 
 +something other than eventually dying of it — **a wound is paid for in 
 +rations**. 
 + 
 +One addition to NetWhack: it needs ''EN_REGEN'' (500) in the tank. Starving 
 +and mending at once is the single combination that lets a player wait out any 
 +wound for free, because the waiting is what heals them. Verified: **0** hit 
 +points healed over ten thousand turns below the line. 
 + 
 +The cost is only taken when a point actually goes back. NetWhack rolls, calls 
 +''heal()'' and does ''hunger++'' regardless — ''heal()'' clamps, so an 
 +unwounded player pays a point of food for nothing. That is an artifact of two 
 +statements sitting next to each other, not a design. 
 + 
 +Healing needed something to stop at, so the player has an ''hp_max'' and every 
 +path that gives points back goes through one clamping routine. 
 + 
 +What is left: **resting** to pass turns safely, and a proper **death**  
 +tombstone, final score, and a return to the prompt. Starvation and monsters 
 +both drop into ''kill_player'', which prints "You died." and returns.
  
 ===== Part VI — a world worth returning to ===== ===== Part VI — a world worth returning to =====
  
-==== Step 18. A monster table ====+==== Step 18. A monster table — DONE ==== 
 + 
 +There used to be ''make_rat'', ''make_snake'' and ''make_spider'': three 
 +routines, each with its own hit-point roll and its own hard-coded glyph, and a 
 +''random_monster'' that rolled 1..3 and branched to one of them. **Adding a 
 +fourth monster meant writing a fourth routine.** 
 + 
 +Now there is one routine and a table. ''montable.sda'' is all 28 rows of 
 +NetWhack's ''MobData.mobdata[]'', with every field it carries. 
 + 
 +=== Generated, not transcribed === 
 + 
 +The table is produced by ''tools/gen_montable.py'', which reads NetWhack'
 +Java directly — resolving ''monsym'' symbols to glyphs, ''ColorMap'' names to 
 +CGA indices, ''Attack''/''Damage'' constants to numbers, and dice strings like 
 +''"1d2-1"'' to (n, sides, bonus). Twenty-eight rows of symbolic fields is 
 +exactly what gets copied wrong by hand, and NetWhack's table will change 
 +again. Re-run it; do not edit the output. 
 + 
 +''ColorMap'' turned out to be the standard CGA order already, so the colours 
 +came across as-is. 
 + 
 +=== The record === 
 + 
 +  MO_GLYPH  0   MO_AC    3   MO_MSPEED 6   MO_NATK 13 
 +  MO_COLOR  1   MO_PROB  4   MO_ASPEED 8   MO_ATK  14  (3 x 5 bytes) 
 +  MO_LEVEL  2   MO_ALIGN 5   MO_NAME  10   MO_SIZE 29 
 + 
 +An instance keeps two things: ''OBJ_DATA1'' is current hit points, which 
 +change, and ''OBJ_DATA2'' is the **kind**, which does not. Everything else is 
 +read back out of the table from that kind, so an instance costs no more than 
 +it did. 
 + 
 +=== What is wired === 
 + 
 +  * **glyph, colour, name.** Colour is not decoration: 28 monsters share 6 
 +    glyphs, and without it a dog and a wolf are the same ''d''
 +  * **level** — hit points are ''level d8'', as ''Mobile'' rolls them, and 
 +    ''10 x level'' is the attack rating. ''getAttackRating()'' is 
 +    ''10*xp_level + 3*DEX + STR + bonuses''; monsters here have no stat block, 
 +    so the level term is what is left. 
 +  * **armour class**, added to the defence, as ''getDefenseRating()'' does. 
 +  * **probability**, and **the attacks**. 
 + 
 +Every monster used to rate a flat 10 — which is exactly what ''10 x level'' 
 +comes to at level 1. So **the first floor plays as it always did** and the 
 +deeper ones get harder, which is what importing the levels was for. 
 + 
 +Spawning follows ''randomkind(plevel, zlevel)'': the window is 
 +''(plevel + zlevel) / 6'' to ''/ 2'', by rejection sampling as NetWhack does 
 +it. Floors 1 and 2 hold level-1 monsters only; by floor 9 it is anything up to 
 +level 5. There is no experience system yet, so the player's level is 1 and 
 +depth widens the window alone. 
 + 
 +=== What was left behind === 
 + 
 +All of it is **imported**; these are the fields nothing reads yet: 
 + 
 +  * ''MO_MSPEED'' / ''MO_ASPEED'' — there is no speed system. A rock mole at 
 +    1500 and a phase rat at 400 currently move at the same rate. This is the 
 +    big one, and it is a scheduler, not a data problem — which is the point of 
 +    importing the numbers now. 
 +  * ''MO_ALIGN'' — no alignment, no peaceful monsters, no ''Really attack?'' 
 +  * ''MOA_DMGT'' — no damage types, so a salamander's fire is ordinary. 
 +  * ''MOA_TYPE'' is stored and half-used: ''do_attack'' picks **one attack at 
 +    random** from the list, which this does, so a wererat's two attacks and a 
 +    salamander's three already work. What is unused is the claw/bite/spit 
 +    distinction in the message. 
 + 
 +Row 0 is the player. Its probability is 0 so it can never be picked; it is 
 +kept so an index here is an index into ''MobData''
 + 
 +=== Verification === 
 + 
 +A wrong offset in a 29-byte record reads as a monster that is subtly too 
 +strong, not as a crash, so ''test_mon.sda'' asks questions with known answers: 
 + 
 +  depth 1 level window              1 .. 1 
 +  depth 10 level window             1 .. 5 
 +  salamander hit points             14 .. 57   (8d8) 
 +  sewer rat damage                  0 .. 1     (1d2-1) 
 +  battle orc damage, highest        6          (2d3) 
 +  salamander attack / defence       80 / 81    (level 8, ac 1) 
 +  wererat attacks in the table      2 
 +  orc damage over 200 real swings   804        (through do_combat)
  
-Replace hardcoded monsters with data: glyph, name, hit points, damage, speed, +The last one goes through ''do_combat'' rather than calling ''mon_damage'' 
-depth rangebehaviour flagsNetWhack'''MobData'' has 60 entries; dozen +directlybecause that is where a register clobbered across a call shows up. 
-would already transform the game. Spawn by depth so descending means something.+And one did: **''FLD'' is ''FL:D''**, so the pair aliases the ''D'' register, 
 +and using ''D'' as scratch while walking row silently overwrote the low 
 +sixteen bits of the pointer. Every field then read as zero, the code took its 
 +"no attacks" early-out, and **every monster in the game did no damage at 
 +all** — with no crash and no assembler warning''mon_damage'' uses ''C''.
  
 ==== Step 19. Pathfinding ==== ==== Step 19. Pathfinding ====
Line 174: Line 850:
 ''S'' to save and resume. The file services already exist behind ''INT $15''. ''S'' to save and resume. The file services already exist behind ''INT $15''.
 A high score table after that. A high score table after that.
 +
 +===== Part VII — saying it properly =====
 +
 +==== Step 21. Item names in messages - DONE ====
 +
 +Everything says //"You wield it."// The item knows its name; the message does
 +not ask. NetWhack has a small naming layer that every message goes through:
 +
 +  * ''name()''     — the real name, with ''pre_name'' and ''post_name'' when identified
 +  * ''aname()''    — //"a dagger"//, //"an athame"//
 +  * ''tname()''    — //"the dagger"//
 +  * ''bcstatus()'' — //"blessed "// / //"cursed "// / //"uncursed "//, when known
 +  * ''msg.You(s)'' — prints //"You "// + s
 +
 +so ''msg.You("wield " + tname())'' is //"You wield the dagger."// We already
 +build names for potions and scrolls; this generalises that and routes the
 +messages through it.
 +
 +**Do this first.** It is small, it is the difference between a prototype and a
 +game every single turn, and everything below it wants to say something.
 +
 +The message window is 19 columns and wraps mid-word, so this also wants a
 +word-wrapping ''print_msg'' — the hand-broken strings do not survive a name
 +being spliced into the middle of them.
 +
 +===== Part VIII — things that do something =====
 +
 +==== Step 22. Effects, as three things instead of one - DONE ====
 +
 +=== Why not NetWhack's Effect class ===
 +
 +''Effect.java'' is the **only major system in NetWhack that is not a table**.
 +There is an ''ArmorData'', a ''MobData'', a ''PotionData'', a ''TileData'' and
 +a ''WeaponData''. There is no ''EffectData''. Instead there is one class with
 +twelve fields --
 +
 +  ticks  uses  trigger  type  expired  index  value  extra
 +  parent  item  mobile  engine
 +
 +-- of which three (''index'', ''value'', ''extra'') are untyped registers
 +whose meaning changes with the type, and **four separate ''switch (type)''
 +statements** in the same file: ''on_transfer'', ''process'', ''on_remove'' and
 +''changekind''. One effect's behaviour is smeared across four places, and
 +adding one means editing all four.
 +
 +It is also inconsistent with itself about the same job. ''E_E_MODSTAT'' is
 +computed on read -- its ''on_transfer'' and ''on_remove'' are empty, with a
 +comment saying it is handled in ''Mobile.get_attribute()''. ''E_E_DEFENSE'',
 +three cases away, caches: ''eAC += value'' on transfer and ''-= value'' on
 +remove. **Two opposite strategies for the same job in one switch.**
 +
 +A table row tells you when you are finished: the row is full. An object with
 +three general-purpose registers and no schema never does. That is what makes
 +the design feel open-ended -- there is no point at which it says //done//, so
 +every new effect reopens the whole question.
 +
 +=== It was doing three unrelated jobs ===
 +
 +^ NetWhack's ^ what it actually is ^
 +| ''MODSTAT'', ''DEFENSE'', ''PRE_ATP'' | derived state, not an event |
 +| ''SPEED'', ''BLIND'', ''TEMPORAL_SUSPENSION'' | genuinely "for N turns, X" |
 +| ''EDNAS_PIES'' | seven lines of prose -- a cutscene in an effect costume |
 +
 +The pie is the tell. It became an Effect because Effect was the only hook
 +available; the system attracted things that did not belong in it.
 +
 +So the three are split, and each goes where the game already keeps that kind
 +of thing.
 +
 +=== 1. Continuous modifiers: computed, never stored ===
 +
 +''attack_rating'' and ''defense_rating'' already walk the equipment rack
 +adding terms up. A condition is **one more term in that walk**. Nothing is
 +applied and nothing has to be un-applied.
 +
 +That deletes a whole class of bug. ''eAC += value'' / ''eAC -= value'' is
 +wrong for ever if the effect is removed twice, or if the value changes while
 +it is worn, or if a save reorders things. A number computed on read cannot
 +desynchronise. NetHack computes AC from worn armour every time for exactly
 +this reason.
 +
 +=== 2. Timed conditions: a flat array of counters ===
 +
 +''cond.sda''. One slot per kind, holding **turns remaining** -- NetHack's
 +''u.uprops[]''. Setting one is a store, ticking them is one loop in
 +''sched_turn'', expiring one is a compare against zero. No objects, no list,
 +no ''expired'' flag, no ''gc()''.
 +
 +  .equ CD_NAME      ; 3 bytes: what to call it
 +  .equ CD_START  3    ; 3 bytes: what to say when it begins
 +  .equ CD_END    6    ; 3 bytes: ... and when it wears off
 +  .equ CD_SIZE   9
 +
 +  .equ C_BLIND      ; Effect.E_P_BLIND
 +  .equ C_FAST    1    ; Effect.E_P_SPEED
 +
 +That is the schema the Effect class never had, and **a row is finished when
 +those three pointers are filled in**.
 +
 +The API is four routines: ''cond_set(kind, turns)'', ''cond_on(kind)'',
 +''cond_tick()'' and ''cond_slot(kind)''. ''cond_set'' only says the start
 +message when the condition was not already true, so a second potion of speed
 +lengthens the haste rather than announcing it twice.
 +
 +=== Read where it matters, applied nowhere ===
 +
 +This is the part worth keeping hold of. A condition is never pushed into
 +anything -- the one place that cares **asks**:
 +
 +  * **Blindness** is asked about in ''los_update'', which then marks only the
 +    square you stand on. NetWhack does ''mobile.blind++'' on transfer and
 +    ''blind--'' on remove, and has to get both right for ever.
 +  * **Haste** is asked about in ''player_charge'', where an action costs half
 +    the clock. NetWhack's ''E_P_SPEED'' instead does ''action_time -= value''
 +    every turn //from inside the effect// -- the same idea pushed the other
 +    way round, the effect reaching into the mobile rather than the mobile
 +    asking the effect.
 +
 +=== 3. One-shot moments: the item's own routine ===
 +
 +''potion_effect'' in ''food.sda'': a dispatch on kind, like ''mon_damage'' and
 +''item_name'' already are. A potion of healing heals you at the point where
 +you drank it, and does not need to become an object with a lifecycle first.
 +Each potion's whole behaviour is readable in one piece.
 +
 +Nutrition is not in there -- every potion has some and ''do_quaff'' has
 +already applied it, which is why a potion of water does nothing else at all.
 +
 +=== Verification ===
 +
 +  blind on after cond_set        1      turns after 2 of 5      3
 +  blind on after 5 ticks              action, normal       1000
 +  action, hasted               500      healing, 2d6 from 1     9
 +  poison, 2d6 from 20           17      blind from potion     250
 +  speed from potion            100      second potion         200
 +
 +and in the real game, poked blind and stepped: lit floor **194 to 0**, all 195
 +squares still remembered in dark grey, items drawn **2 to 0**. You keep the
 +map you know and see none of it.
 +
 +=== What is left ===
 +
 +''TEMPORAL_SUSPENSION'' and the pie have no slots yet; both are one row and a
 +case when they are wanted. The nine NetWhack effect types are otherwise
 +covered, with less machinery than the Effect class alone.
 +
 +==== Step 23. The speed system - DONE ====
 +
 +''MobInfo'' carries ''mspeed'' and ''aspeed'' and has since Step 18 — a rock
 +mole is 1500, a phase rat 400, the player 1000 — and **nothing reads either**.
 +Every mobile moves once per turn, so a phase rat is exactly as quick as a
 +rock mole.
 +
 +NetWhack's own comment says how it is meant to work: //"relative speed can be
 +added by increasing a speed variable and only allowing movement when it
 +reaches a certain value (then resetting it)"//. An energy counter per mobile,
 +topped up each turn by its speed, and it acts while it can afford to. That is
 +also what turns ''gametime'' into the fine-grained clock its
 +''% TICKS_PER_TURN'' assumes — ours counts player turns because nothing needed
 +finer.
 +
 +This is the one imported field that changes how the game //plays// rather than
 +how it reads, and the potion of speed has nothing to do without it.
 +
 +**Taken before Step 22**, because half of the ''Effect'' triggers --
 +''T_PERTICK'', ''T_PERTURN'', ''T_PERSTEP'' -- have nothing to fire against
 +until a clock exists.
 +
 +=== There is no event queue ===
 +
 +Worth writing down, because it is easy to misremember: **NetWhack has no
 +global event list at all.** No event class, nothing scheduled centrally.
 +''Engine.do_tick(m)'' is
 +
 +  m.per_tick();
 +  m.action_time--;
 +  if (m.action_time > 0) return;   // no energy yet
 +  ...act...
 +  m.action_time += m.movespeed;    // and pay for it
 +
 +per mobile per gametick, and effects live in ''m.fxlist'' on the mobile that
 +owns them, processed by trigger and swept by ''gc()''. Nothing reaches out of
 +an item into the engine. So the counter-per-mobile is kept exactly as it is.
 +
 +=== What is not kept: the ticking ===
 +
 +''do_tick'' runs for every mobile on every one of the thousand gameticks. In
 +Java that is free. Twenty mobiles x a thousand ticks x ten instructions is
 +**200,000 instructions a turn** -- about 154ms at the 1.30M instructions/sec
 +this machine measures at, on top of the 48ms Step 11 already costs, and it
 +grows with the monster count.
 +
 +So time does not advance one unit at a time. The **smallest** counter is
 +found, that much is taken off every counter at once, and whoever reaches zero
 +acts. Identical arithmetic -- 900 still acts more often than 1000 -- for one
 +pass per action instead of a thousand passes per turn. The 1000 scale is kept;
 +the resolution lives in the arithmetic, not in the loop count.
 +
 +=== A field, not a list ===
 +
 +''OBJ_READY'' is a field in each record rather than an entry in a central
 +list, and that is the important choice. A list needs entries removed when a
 +monster dies, a level changes, or a node is recycled -- and a missed removal
 +is a stale event pointing at whatever now occupies that node. **A counter in
 +the record dies with the record.** It is also nearly free: the world queue is
 +already walked every turn by ''move_mon'', ''draw_items'' and
 +''pick_up_items'', so this is one more comparison on a scan that was happening
 +anyway. Only ''OT_MONSTER'' records are scanned; items on the floor do not act.
 +
 +''move_mon'' charges the monster **before** it moves, so every way out of the
 +routine has been paid for. Otherwise one wedged against a wall never advances
 +its counter and the scheduler hands it every turn for ever.
 +
 +A turn is still ''SPD_PLAYER'' units of clock, so hunger, regeneration and the
 +wandering-monster roll fire exactly as often as they did -- a loop rather than
 +an if, since one slow action can cross two turns.
 +
 +=== Verification ===
 +
 +The claim is a ratio, so it is counted rather than looked at. 400 scheduler
 +steps with three actors:
 +
 +  player     (1000)   120 acts
 +  phase rat   (400)   300 acts     = 120 x 1000/400
 +  rock mole  (1500)    80 acts     = 120 x 1000/1500
 +
 +and in the real loop with twenty monsters running, 97 turns left the energy
 +meter at exactly 5000 - 97.
 +
 +''OBJ_LEN'' went 37 to 39 for the counter, so the 4 KB node array holds 105
 +rather than 110.
 +
 +==== Step 24. Potions that do what they say - DONE ====
 +
 +Healing, poison, blindness and speed, all four landed with Step 22, since
 +''potion_effect'' is the shape that step decided on: a dispatch on kind, one
 +case per potion, each readable in one piece.
 +
 +  healing      2d6 back, clamped by heal_player
 +  poison       2d6 off, floored at 0, and -250 nutrition from the table
 +  blindness    C_BLIND for 250 turns
 +  speed        C_FAST for 100 turns
 +  water        nothing at all -- its 100 nutrition is the whole of it
 +
 +Blindness did **not** want ''LOS_RADIUS'' set to 0, as this step guessed
 +before it was written. ''los_update'' asks ''cond_on(C_BLIND)'' and marks only
 +the square you stand on -- a temporarily-modified global would have to be put
 +back, and putting things back is the failure mode Step 22 exists to avoid.
 +
 +==== Step 25. Scrolls, and ''r'' to read - DONE ====
 +
 +''Scroll.event_read'', in the shape Step 22 settled on: a dispatch on kind,
 +one case per scroll, each readable in one piece.
 +
 +^ scroll ^ what it does ^
 +| identify | learn what one thing in your pack is |
 +| teleport | somewhere else on this level |
 +| town portal | says so -- there is no town until Step 30 |
 +| crumpled note | reads it, and does **not** vanish |
 +| temporal suspension | everything else waits 5 to 20 turns |
 +
 +Reading identifies the scroll, so the message uses the name it had **before**
 +you read it: //"You read the scroll of gibberish."// NetWhack only
 +self-identifies the teleport one, inside ''do_teleport'', which looks like an
 +omission rather than a decision -- you plainly learn what a scroll was by
 +watching what it did.
 +
 +=== Temporal suspension is not a condition ===
 +
 +NetWhack does ''action_time -= Dice.roll(5,20) * 1000'', giving the player
 +credit so that everyone else has to wait. Our counters are unsigned and count
 +**down**, so the same thing is expressed from the other side: everybody else
 +is pushed back by that much. Identical in effect, and it cannot go negative.
 +
 +It also **must not** be a condition that makes the player's actions free.
 +''sched_find'' would then return zero for ever, so the clock would never
 +advance, so ''sched_turn'' would never fire, so ''cond_tick'' would never run
 +-- and the suspension would never end. A condition has to be something the
 +clock can outlive.
 +
 +=== A bug inherited and not copied ===
 +
 +''sc_identify'' does what NetWhack's ''do_id'' meant to do. That one builds
 +its list of candidates with
 +
 +  if (i.identified == false);
 +      a_list.add(i);
 +
 +-- a stray semicolon, so the ''add'' is unconditional and the scroll can spend
 +itself telling you about something you already knew.
 +
 +=== Verification ===
 +
 +  unknown kinds in the pack     2 -> 1
 +  teleport moved the player     yes, and to somewhere walkable (3 runs)
 +  clock added by suspension     8000 / 18000 / 11000
 +
 +=== Two hazards of the assembler, found the hard way ===
 +
 +''LDBL AL'' and ''LDAL XL'' are **not** register moves, and the assembler
 +takes both without a word. The first made identify choose nothing; the second
 +stored garbage into ''PX'' and ''PY'', so teleport put the player inside a
 +wall -- and only showed up because the test asked whether the destination was
 +walkable rather than only whether he had moved. ''MOV'' is the register move;
 +''LDxx'' loads an immediate or from memory.
 +
 +==== Step 26. Blessed, cursed, uncursed ====
 +
 +Three bits per item and ''bcstatus()'' in front of the name. Cursed armour
 +that will not come off is the first thing in the game that can go **wrong**
 +in an interesting way.
 +
 +===== Part IX — a world =====
 +
 +==== Step 27. Tiles store their kind - DONE ====
 +
 +A square used to store **the character it looked like**. It stores its
 +**kind** now, and the glyph is one column of a generated table along with the
 +colour, the flags, the name and the description. Same two bytes per square.
 +
 +=== Why this had to come before the village ===
 +
 +''TileData'' draws chair, bridge, bed, road, table and throne **all as ''='''',
 +and wall, secret door and altar **all as ''#''''. A map that stores glyphs
 +cannot tell a bridge from a chair -- so it cannot say whether you may walk on
 +it, what colour to draw it, or what it is called when you look at it.
 +
 +The map SOURCE alphabet is a different thing and is unambiguous: ''r'' road,
 +''B'' shop counter, ''d'' bed, ''f'' flowers, ''b'' bridge, ''='' chair,
 +''s'' secret door, ''*'' water, ''T'' tree. That is why a level can be written
 +as text at all. ''src_to_kind'' is ''DungeonMaker'''s switch, one character to
 +one kind; only the DISPLAY collides.
 +
 +=== The table ===
 +
 +''tiletable.sda'', generated by ''tools/gen_tiletable.py'':
 +
 +  .equ TI_GLYPH  0    ; 1 byte : what it is drawn as
 +  .equ TI_COLOR  1    ; 1 byte : and in what colour
 +  .equ TI_FLAGS  2    ; 1 byte : TF_WALKABLE | TF_OPAQUE
 +  .equ TI_TNAME  3    ; 3 bytes: -> its name, for looking at it
 +  .equ TI_DESC      ; 3 bytes: -> the long description. ON TAP
 +  .equ TI_TSIZE  9
 +
 +The glyph, colour, name and description come from ''TileData''. **The flags do
 +not** -- walkable and vblock are set by a switch in ''Tile.changekind()'' and
 +are not in the table at all. The generator reads both files, so the two cannot
 +drift apart.
 +
 +Two deliberate departures, both made in the generator where they are visible
 +rather than in the data where they would look like the source:
 +
 +  * Four kinds are drawn with **box-drawing characters** and two with a
 +    **space**. Neither survives an ASCII renderer, and a space is what an
 +    unseen square looks like -- a shop floor drawn as one would be invisible.
 +    Those six get stated ASCII stand-ins.
 +  * A **wall is light grey**, not ''TileData'''s ''DARK_GRAY'', which is
 +    exactly the colour a remembered square is drawn in. A secret door matches
 +    the wall it is pretending to be.
 +
 +=== What it cost ===
 +
 +''tile_flags_for'' used to test for ''#'' and ''+'' and call everything else
 +floor -- as far as two glyphs could take it. ''get_glyph'' looks the glyph up;
 +''put_glyph'' became ''put_kind''; ''draw_world'', ''open_door'',
 +''place_stairs'', ''expand_level'' and the whole dungeon maker read and write
 +kinds. ''tiletable.sda'' has to be assembled **before** ''map.sda'', because
 +''.equ'' has no forward references and ''TK_TEMP'' is derived from ''TK_COUNT''.
 +
 +=== And Brynn ===
 +
 +The village is the first level: ''Brynn.java'', **84 x 30**, verbatim but for
 +trimming each row to the 84 columns it declares -- the Java rows are one
 +character longer. ''popfreq'' 0, because nothing wanders into a town.
 +
 +Its well is **not in the map text**; NetWhack adds it in code. Read the code
 +and not the comment above it: the comment says //"Add the well at 24,4"// and
 +the three lines below say ''s.xpos = 4; s.ypos = 28''. **Lower left.** That is
 +the level's ''LV_DOWN''.
 +
 +=== Where does NetWhack put the player? Nowhere ===
 +
 +This is worth writing down because it cannot be found by looking for it.
 +''Level.java'' declares
 +
 +  public int px_last = 0, py_last = 0;
 +
 +and line 519 does ''pc.xpos = px_last''. **Brynn never assigns either.** So the
 +answer is the field initialiser: NetWhack starts you in the top-left corner.
 +Ours starts on the road at **2,6**, which is a decision rather than an
 +accident.
 +
 +That needed a new field. ''LV_UP'' was doing two jobs -- where the stairs up
 +are, and where you appear -- and Brynn has no stairs up, so the two had to
 +come apart. ''LV_START'' is where a new game begins; ''LV_SIZE'' 23 to 25.
 +
 +**0,0 means "no such staircase."** ''place_stairs'' skips one whose
 +coordinates are 0,0, that being the one square no real staircase can occupy --
 +the map corner on any level with a border. This replaced a test on
 +''level_index'', which did not work: ''load_all_levels'' walks the levels with
 +''TL'' and never updated ''level_index'' while building, so the test read a
 +stale value. (It does now.)
 +
 +Checked on the tile array rather than by eye: exactly one staircase on Brynn,
 +kind 7 at (4,28), and no kind 6 anywhere.
 +
 +The 80 x 40 scratch room is gone, and so are the starting dagger, armour and
 +ration -- scaffolding for testing the item system, which the shops will
 +replace. ''x'' still conjures one item from the whole table.
 +
 +Trees draw **green**, water **blue**, roads **brown**, flowers **grey**, each
 +in a lit and a remembered shade. None of that was expressible before.
 +
 +=== What Brynn still has not got ===
 +
 +The map only. The ''DENIZEN'' and ''SHOPKEEPER'' lines that follow it in the
 +Java are Step 28, and the people they describe are Steps 31 and 32. The
 +village is a place; it is not yet inhabited.
 +
 +==== Step 28. A level script, and people who answer questions - DONE ====
 +
 +A level's map text is now followed by directives:
 +
 +  DENIZEN Farmer_Jim 19 17
 +  CHAT  The_apples_look_lovely_this_year.
 +  REPLY job   I_work_the_orchard._Apples,_mostly._It's_a_living.
 +  REPLY name  I'm_Jim._Farmer_Jim,_on_account_of_the_farm.
 +
 +''DENIZEN'', its inline chats and ''NOMONSTERS'' are NetWhack's, read the way
 +''DungeonMaker'' reads them -- split on spaces, ''_'' standing in for a space
 +inside a token -- so **Brynn's own four denizens parse as written**. ''CHAT''
 +and ''REPLY'' are ours.
 +
 +=== Why a directive and not punctuation ===
 +
 +''job:I'm_a_farmer'' reads well and would work. Split on the **first** colon
 +and the answer can contain as many more as it likes, so no escape is needed
 +for the colon at all -- a keyword is one word and cannot contain one.
 +
 +Two things argued against it:
 +
 +  * The format already has **exactly one escape**, ''_'' for space. A
 +    backslash rule would be a second one to learn, and every directive added
 +    later would inherit both.
 +  * Inferring the //kind// of speech from a punctuation mark cannot tell a
 +    keyword from a random line that happens to contain a colon.
 +
 +A directive says which it is instead of leaving it to be guessed, and the next
 +thing we want -- ''GIVE'', ''QUEST'', ''SHOP'' -- is another word rather than
 +another mark.
 +
 +=== Talking: Ultima IV's three cases ===
 +
 +NetWhack has only the middle one. An NPC there holds a list of lines and says
 +one at random; there is no interactive speech in it anywhere. The other two
 +are Rogueima's.
 +
 +^ case ^ what happens ^
 +| nothing to say | //"They do not seem to want to talk."// |
 +| random speech | Farmer Jim says, "The apples look lovely this year." |
 +| keyword replies | a page of its own, and you type words at it |
 +
 +  Farmer Jim -- and you are talking to them.
 +
 +    I work the orchard. Apples, mostly. It's a living.
 +
 +    Ask about a subject -- try NAME, or JOB. BYE to stop.
 +    Say:
 +
 +An unknown word gets //"They shrug, and say nothing about that."//; ''BYE'' or
 +an empty line ends it. Matching folds case, so ''JOB'' and ''job'' are the
 +same question.
 +
 +''npc_mode'' makes the three-way choice **a value rather than a shape of
 +code**, so it can be checked without reading a screen. Replies win over chats:
 +somebody who will hold a conversation should not be reduced to muttering one
 +line at you.
 +
 +=== Nothing copies text ===
 +
 +The script stays in the program image and an NPC holds **pointers into it**.
 +''print_script'' turns the underscores back into spaces on the way out, rather
 +than rewriting the source the way ''replace('_',' ')'' does -- so a level can
 +be built twice without its script having been consumed. A keyword ends at the
 +space that follows it in the script, since it has no terminator of its own.
 +
 +=== Verification ===
 +
 +  denizens created          4        chat lines             21
 +  keyword replies          20        "JOB" finds an answer   yes
 +  "wombat" does not       yes        modes: 2, then 1, then 0
 +
 +The three modes are read off one denizen stripped in stages: with both, it
 +converses; with its replies taken away it falls back to random; with its chats
 +gone too it has nothing to say.
 +
 +=== An ordering bug ===
 +
 +''init_objects'' and ''npc_init'' ran **after** ''load_all_levels'', so the
 +script put four denizens into the node array and ''init_objects'' immediately
 +emptied it again -- ''npc_count'' read 0 and the village was deserted.
 +Everything a level writes into has to exist before the level is built.
 +
 +==== Step 29. Branches, and stairs that know where they go ====
 +
 +The dungeon is not one stack. A staircase carries a **destination branch**:
 +
 +  Brynn --(the well)--> BrynnWell --> LCave
 +                                  --> BDungeon --(depth 9)--> Croky Castle
 +
 +''LevelFactory.createlevel'' takes a branch name and a depth and dispatches on
 +it; ''LevelLibrary'' keeps the levels already built so going back finds them
 +as you left them. Our ''level_table'' is a flat list of five and will need to
 +become that.
 +
 +==== Step 30. The village of Brynn ====
 +
 +Depth 0, ''popfreq'' 0 — no wandering monsters, which is what a town //is//.
 +The well in the middle is a staircase into ''BrynnWell''. Needs Steps 27, 28
 +and 29 first, and then it is mostly data.
 +
 +==== Step 31. NPCs and chat ====
 +
 +''mobile/NPC.java''. A denizen has a name, a position and a list of things to
 +say; ''t'' picks one at random. ''Fortune'' supplies a rumour when the script
 +says ''RANDOM''. Rufus the dog lives here too — the first mobile that is
 +neither the player nor an enemy.
 +
 +The four denizens of Brynn are how the sunsword quest is told: Farmer Jim, the
 +Mayor, Edna and Father Monoly each know a piece of it.
 +
 +==== Step 32. Shopkeepers ====
 +
 +''engine/Shop.java''. Buy, sell, and a shopkeeper who objects when you leave
 +with something you have not paid for. The paged list it uses is
 +''item_pagedisplay'' — **the screen we already built in Step 12's paging**, so
 +this is the shop logic and not the shop interface. ''IT_VALUE'' has been sitting
 +in the item table unread since the import, waiting for exactly this.
 +
 +===== Part X — the quest =====
 +
 +==== Step 33. Croky Castle ====
 +
 +The goal level, and the only one with no stairs down. Reached from ''BDungeon''
 +depth 9, but only while the sunsword has not been found.
 +
 +==== Step 34. The sunsword ====
 +
 +''WeaponData[15]'', probability 0 so it is never generated at random — it is
 +**placed**. Wielding it sets ''GameFlags.has_sunsword'', says //"The sunsword
 +seems to glow and gleam with an unearthly light!"//, and from then on it
 +**attracts monsters**: ''Engine.gametick'' pops an extra one every hundred
 +turns with //"You've got a bad feeling about this..."//
 +
 +The first unique object in the game, and the first item whose being carried
 +changes the rules.
 +
 +==== Step 35. The endgame ====
 +
 +Carry it back to the surface. ''Engine'' checks, every time you take a
 +staircase, whether you have the sunsword and are standing in ''Start'' — and
 +if so, +1000 and //"You have escaped the dungeons of doom!"// That is the win
 +condition, and it is four lines. It needs Step 17's death screen to exist,
 +because winning and dying print the same tombstone.
 +
 +===== Part XI — depth, once it is a game =====
 +
 +==== Step 36. Traps ====
 +
 +Six: trapdoor, bear trap, teleport, dart, sleeping gas, rust. A tile flag, a
 +kind, and a hidden bit.
 +
 +==== Step 37. Search, and secret doors ====
 +
 +''s'', and ''TileData.SECDOOR''. BrynnWell was hand-converted with its secret
 +doors turned into ordinary ones back in Step 8 //because there was no search
 +command//; this is the step that lets them go back.
 +
 +==== Step 38. Experience, and the stat block ====
 +
 +''Stats'': STR, DEX, INT. The ratings are already the right shape and simply
 +have the terms missing — ''getAttackRating()'' is ''10*xp_level + 3*DEX + STR
 ++ bonuses'', and ours is ''player_str'' standing in for all of it. Killing
 +things should raise a level, and the monster spawn window already reads
 +''plevel'' and has been given a hard-coded 1 since Step 18.
 +
 +==== Step 39. LCave, and a third kind of level ====
 +
 +The cave generator, skipped in Step 9. Gives ''BrynnWell'' somewhere to branch
 +to that is not more of the same.
 +
 +==== Step 40. Saving ====
 +
 +''S'' to save and resume, and ''ObjSaver'' for the shape of it. The file
 +services already exist behind ''INT $15''. The queues make this harder than it
 +looks: what is saved is a graph of pointers into a fixed node array, so it
 +saves as indices or not at all.
 +
 +==== Step 41. The rest of the flavour ====
 +
 +''Fortune'' rumours, Edna's pies, the dogfood, the well, the altar and the
 +throne. None of it is systems work; all of it is what makes the place feel
 +like somewhere rather than a grid.
  
 ===== If only three steps get done ===== ===== If only three steps get done =====
Line 180: Line 1459:
 of sight. That is the difference between a demo and a roguelike, and none of of sight. That is the difference between a demo and a roguelike, and none of
 the three is large. the three is large.
 +
 +===== And if only three MORE get done =====
 +
 +**Steps 21, 22 and 29** — names in messages, the effects hook, and branching
 +stairs. The first makes every turn read like a game; the second is what every
 +consumable in the table is waiting for; the third is the shape the whole rest
 +of the world hangs off.
  
sd/rogueima_plan.1788793192.txt.gz · Last modified: by 127.0.0.1

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki