sd:rogueima_plan
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| sd:rogueima_plan [2026/09/08 08:04] – external edit 127.0.0.1 | sd:rogueima_plan [2026/09/09 03:27] (current) – external edit 127.0.0.1 | ||
|---|---|---|---|
| Line 853: | Line 853: | ||
| ===== Part VII — saying it properly ===== | ===== Part VII — saying it properly ===== | ||
| - | ==== Step 21. Item names in messages ==== | + | ==== Step 21. Item names in messages |
| Everything says //"You wield it."// The item knows its name; the message does | Everything says //"You wield it."// The item knows its name; the message does | ||
| Line 877: | Line 877: | ||
| ===== Part VIII — things that do something ===== | ===== Part VIII — things that do something ===== | ||
| - | ==== Step 22. The effects hook ==== | + | ==== Step 22. Effects, as three things instead of one - DONE ==== |
| - | '' | + | === Why not NetWhack's Effect |
| - | **trigger**, | + | |
| - | something happens to them: | + | |
| - | E_MODSTAT | + | '' |
| - | | + | There is an '' |
| + | a '' | ||
| + | twelve fields -- | ||
| - | | + | |
| - | T_FOOD | + | parent |
| - | '' | + | -- of which three ('' |
| - | T_FOOD)'' | + | whose meaning changes with the type, and **four separate |
| - | machinery is a fixed array of effect slots on the player, a dispatch on type, | + | statements** in the same file: '' |
| - | and '' | + | '' |
| + | adding one means editing all four. | ||
| - | ==== Step 23. The speed system ==== | + | It is also inconsistent with itself about the same job. '' |
| + | computed on read -- its '' | ||
| + | comment saying it is handled in '' | ||
| + | three cases away, caches: '' | ||
| + | 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' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | |||
| + | 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 === | ||
| + | |||
| + | '' | ||
| + | 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. '' | ||
| + | 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 === | ||
| + | |||
| + | '' | ||
| + | '' | ||
| + | '' | ||
| + | no '' | ||
| + | |||
| + | .equ CD_NAME | ||
| + | .equ CD_START | ||
| + | .equ CD_END | ||
| + | .equ CD_SIZE | ||
| + | |||
| + | .equ C_BLIND | ||
| + | .equ C_FAST | ||
| + | |||
| + | 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: '' | ||
| + | '' | ||
| + | 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 '' | ||
| + | square you stand on. NetWhack does '' | ||
| + | '' | ||
| + | * **Haste** is asked about in '' | ||
| + | the clock. NetWhack' | ||
| + | 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 === | ||
| + | |||
| + | '' | ||
| + | '' | ||
| + | you drank it, and does not need to become an object with a lifecycle first. | ||
| + | Each potion' | ||
| + | |||
| + | Nutrition is not in there -- every potion has some and '' | ||
| + | already applied it, which is why a potion of water does nothing else at all. | ||
| + | |||
| + | === Verification === | ||
| + | |||
| + | blind on after cond_set | ||
| + | blind on after 5 ticks | ||
| + | action, hasted | ||
| + | poison, 2d6 from 20 | ||
| + | speed from potion | ||
| + | |||
| + | 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 === | ||
| + | |||
| + | '' | ||
| + | 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 | ||
| '' | '' | ||
| Line 912: | Line 1014: | ||
| how it reads, and the potion of speed has nothing to do without it. | how it reads, and the potion of speed has nothing to do without it. | ||
| - | ==== Step 24. Potions that do what they say ==== | + | **Taken before |
| + | '' | ||
| + | until a clock exists. | ||
| - | Healing, poison, blindness, speed. Until this lands a potion of speed is a | + | === There is no event queue === |
| - | drink of water with a different name on it. Blindness wants '' | + | |
| - | temporarily set to 0, which is one variable; speed wants Step 23's clock. | + | |
| - | ==== Step 25. Scrolls, and '' | + | Worth writing down, because it is easy to misremember: |
| + | global event list at all.** No event class, nothing scheduled centrally. | ||
| + | '' | ||
| - | '' | + | m.per_tick(); |
| - | crumpled note that starts identified. '' | + | m.action_time--; |
| - | the chooser we already have and marks one kind known. Reading | + | if (m.action_time > 0) return; |
| - | the scroll | + | ...act... |
| - | finally paying | + | m.action_time += m.movespeed; |
| + | |||
| + | per mobile per gametick, and effects live in '' | ||
| + | owns them, processed by trigger and swept by '' | ||
| + | an item into the engine. So the counter-per-mobile is kept exactly as it is. | ||
| + | |||
| + | === What is not kept: the ticking === | ||
| + | |||
| + | '' | ||
| + | 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/ | ||
| + | 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 === | ||
| + | |||
| + | '' | ||
| + | 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 | ||
| + | the record dies with the record.** It is also nearly free: the world queue is | ||
| + | already walked every turn by '' | ||
| + | '' | ||
| + | anyway. Only '' | ||
| + | |||
| + | '' | ||
| + | routine has been paid for. Otherwise one wedged against a wall never advances | ||
| + | its counter | ||
| + | |||
| + | A turn is still '' | ||
| + | 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 | ||
| + | phase rat | ||
| + | rock mole (1500) | ||
| + | |||
| + | and in the real loop with twenty monsters running, 97 turns left the energy | ||
| + | meter at exactly 5000 - 97. | ||
| + | |||
| + | '' | ||
| + | rather than 110. | ||
| + | |||
| + | ==== Step 24. Potions that do what they say - DONE ==== | ||
| + | |||
| + | Healing, poison, blindness and speed, all four landed with Step 22, since | ||
| + | '' | ||
| + | case per potion, each readable in one piece. | ||
| + | |||
| + | healing | ||
| + | poison | ||
| + | blindness | ||
| + | speed C_FAST for 100 turns | ||
| + | water nothing at all -- its 100 nutrition is the whole of it | ||
| + | |||
| + | Blindness did **not** want '' | ||
| + | before it was written. '' | ||
| + | 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 '' | ||
| + | |||
| + | '' | ||
| + | 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 | ||
| + | you read it: //"You read the scroll | ||
| + | self-identifies the teleport one, inside '' | ||
| + | omission rather than a decision -- you plainly learn what a scroll was by | ||
| + | watching what it did. | ||
| + | |||
| + | === Temporal suspension | ||
| + | |||
| + | NetWhack does '' | ||
| + | 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' | ||
| + | '' | ||
| + | advance, so '' | ||
| + | -- and the suspension would never end. A condition has to be something the | ||
| + | clock can outlive. | ||
| + | |||
| + | === A bug inherited and not copied === | ||
| + | |||
| + | '' | ||
| + | its list of candidates with | ||
| + | |||
| + | if (i.identified == false); | ||
| + | a_list.add(i); | ||
| + | |||
| + | -- a stray semicolon, so the '' | ||
| + | itself | ||
| + | |||
| + | === Verification === | ||
| + | |||
| + | unknown kinds in the pack 2 -> 1 | ||
| + | teleport moved the player | ||
| + | clock added by suspension | ||
| + | |||
| + | === Two hazards of the assembler, found the hard way === | ||
| + | |||
| + | '' | ||
| + | takes both without a word. The first made identify choose nothing; the second | ||
| + | stored garbage into '' | ||
| + | wall -- and only showed up because the test asked whether the destination was | ||
| + | walkable rather than only whether he had moved. '' | ||
| + | '' | ||
| ==== Step 26. Blessed, cursed, uncursed ==== | ==== Step 26. Blessed, cursed, uncursed ==== | ||
| Line 934: | Line 1164: | ||
| ===== Part IX — a world ===== | ===== Part IX — a world ===== | ||
| - | ==== Step 27. The rest of the tile kinds ==== | + | ==== 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 | ||
| + | colour, the flags, the name and the description. Same two bytes per square. | ||
| + | |||
| + | === Why this had to come before the village === | ||
| + | |||
| + | '' | ||
| + | and wall, secret door and altar **all as ''#'''' | ||
| + | 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: | ||
| + | '' | ||
| + | '' | ||
| + | as text at all. '' | ||
| + | one kind; only the DISPLAY collides. | ||
| + | |||
| + | === The table === | ||
| + | |||
| + | '' | ||
| + | |||
| + | .equ TI_GLYPH | ||
| + | .equ TI_COLOR | ||
| + | .equ TI_FLAGS | ||
| + | .equ TI_TNAME | ||
| + | .equ TI_DESC | ||
| + | .equ TI_TSIZE | ||
| + | |||
| + | The glyph, colour, name and description come from '' | ||
| + | not** -- walkable and vblock are set by a switch in '' | ||
| + | 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 '' | ||
| + | exactly the colour a remembered square is drawn in. A secret door matches | ||
| + | the wall it is pretending to be. | ||
| + | |||
| + | === What it cost === | ||
| + | |||
| + | '' | ||
| + | floor -- as far as two glyphs could take it. '' | ||
| + | '' | ||
| + | '' | ||
| + | kinds. '' | ||
| + | '' | ||
| + | |||
| + | === And Brynn === | ||
| + | |||
| + | The village is the first level: '' | ||
| + | trimming each row to the 84 columns it declares -- the Java rows are one | ||
| + | character longer. '' | ||
| + | |||
| + | 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, | ||
| + | the three lines below say '' | ||
| + | the level' | ||
| + | |||
| + | === Where does NetWhack put the player? Nowhere === | ||
| + | |||
| + | This is worth writing down because it cannot be found by looking for it. | ||
| + | '' | ||
| + | |||
| + | public int px_last = 0, py_last = 0; | ||
| + | |||
| + | and line 519 does '' | ||
| + | answer is the field initialiser: | ||
| + | Ours starts on the road at **2,6**, which is a decision rather than an | ||
| + | accident. | ||
| + | |||
| + | That needed a new field. '' | ||
| + | are, and where you appear -- and Brynn has no stairs up, so the two had to | ||
| + | come apart. '' | ||
| + | |||
| + | **0,0 means "no such staircase." | ||
| + | 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 | ||
| + | '' | ||
| + | '' | ||
| + | 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. '' | ||
| + | |||
| + | 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 '' | ||
| + | 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' | ||
| + | |||
| + | DENIZEN Farmer_Jim 19 17 | ||
| + | CHAT The_apples_look_lovely_this_year. | ||
| + | REPLY job | ||
| + | REPLY name I' | ||
| + | |||
| + | '' | ||
| + | '' | ||
| + | inside a token -- so **Brynn' | ||
| + | and '' | ||
| + | |||
| + | === Why a directive and not punctuation === | ||
| + | |||
| + | '' | ||
| + | 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**, '' | ||
| + | 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 -- '' | ||
| + | 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' | ||
| + | |||
| + | ^ case ^ what happens ^ | ||
| + | | nothing to say | //" | ||
| + | | 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 //" | ||
| + | an empty line ends it. Matching folds case, so '' | ||
| + | same question. | ||
| + | |||
| + | '' | ||
| + | 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**. | ||
| + | '' | ||
| + | than rewriting the source the way '' | ||
| + | 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 === |
| - | altar, throne, shop floor and shop bar, on top of the six we use. Cheap — a | + | |
| - | table of glyph, colour and flags — and every one of them is a prerequisite for | + | |
| - | a village that looks like a village rather than a dungeon with the walls | + | |
| - | knocked out. | + | |
| - | ==== Step 28. A level script ==== | + | denizens created |
| + | keyword replies | ||
| + | " | ||
| - | Brynn is 84x30 of map text followed by directives: | + | 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 === |
| - | " | + | |
| - | " | + | |
| - | Underscores for spaces, because the parser splits on them. '' | + | '' |
| - | has a comment saying all of this //" | + | script |
| - | and attached to the level file, and not written here"// | + | emptied it again -- '' |
| - | '' | + | Everything a level writes into has to exist before the level is built. |
| - | at. | + | |
| ==== Step 29. Branches, and stairs that know where they go ==== | ==== Step 29. Branches, and stairs that know where they go ==== | ||
sd/rogueima_plan.1788854645.txt.gz · Last modified: by 127.0.0.1
