# Battle Mechanics as Regression Tests

Lecture ID: lecture_w24_d01
Week 24, Day 1

## Objectives
- Design minimal clone versions of map loading, script execution, party data, battle turns, item use, and save persistence.
- Connect week 24 material to the overall course plan.

## Transcript
All right, let’s pick up from last week without resetting the whole course in our heads. By now, when I say “battle system,” I do not want you to picture only damage formulas, type charts, and move effects. Those are important, yes, but they are not the architecture. The architecture is the path by which a player-visible event becomes state transition, state transition becomes committed data, and committed data can be checked again later. Today’s lecture is about using battle mechanics as regression tests, which means we are going to treat combat not as an isolated minigame, but as one of the best pressure tests for the entire creature RPG.

Let me say that more concretely. A battle touches party data, item data, scripts, flags, RNG, UI choices, experience or rewards, map return behavior, and save persistence. If your clone architecture can load a map, trigger a script, enter a battle, resolve a turn, use an item, update the party, return to the overworld, and persist the result, you have not merely tested battle. You have tested the backbone of the game.

Now, quick continuity check. In the last stretch of the course, we used Pokemon Showdown as a comparison system, not as our main source of truth for FireRed. We already covered Dex lookup, generation mods, move and species lookup, legality filtering, request state, player choices, action queues, and runMove-style responsibilities. I am not going to re-teach those. Today we are going to use them. In other words, the Dex facade and battle request model become tools for building deterministic tests, while FireRed remains our primary architectural reference for how a complete cartridge-style RPG moves from overworld state into battle and back.

Let’s put something on the board. I’m drawing a horizontal line, left to right. On the far left I’m writing “saved state.” Then “map load.” Then “script.” Then “battle setup.” Then “turn choices.” Then “state assertions.” Then “result commit.” Then “return to map.” Then “save.” That is our week 24 trace. If week 1 asked, “What are the major systems of a creature RPG?” week 24 asks, “Can you prove, with a reproducible trace, that those systems still cooperate after you change one of them?”

This is why battle mechanics make such good regression tests. A regression test is not just a test that passes. It is a small executable memory of behavior you are unwilling to lose. If a Potion heals the active creature by a capped amount, if a fainted creature cannot act, if a scripted trainer battle sets a flag after victory, if the map reloads the player into the correct location afterward, those are not incidental details. Those are promises your architecture is making.

Now, in Pokemon Showdown, the testing material gives us a very clean vocabulary for this. The source focus for today includes pokemon-showdown/test/TESTS.md, chunk_134ac9f75b9d6477, where the testing guide discusses createBattle, seeds, and forceRandomChance. The important architectural lesson is not “copy Showdown.” The lesson is that the test harness must own the initial conditions. It must be able to create a battle with a known format, known teams, known seed behavior, and known random outcomes when necessary. That is how a battle test becomes an executable specification instead of a loose demonstration.

So, imagine I write a test called “Potion used during wild battle heals party member and persists after battle escape.” For a shallow test, I might just instantiate a Pokemon, subtract HP, call usePotion, and assert the HP changed. That is useful, but it is not week 24 useful. A week 24 version creates a minimal world state. It loads a save. It loads a map. It places the player on a tile or facing an event. It runs a script that starts a battle. It seeds the battle. It makes one turn choice. It uses an item. It exits battle. It checks party data. It saves. It reloads. It checks party data again.

That is the difference between testing a function and testing an architecture.

Now, pause here. Check for understanding. If I ask you, “Why not just unit test the item function?” the answer is not that unit tests are bad. The answer is that item behavior is not only item behavior in a creature RPG. Item behavior crosses inventory count, party mutation, battle permissions, UI request state, script result handling, and persistence. The smaller unit test tells you one small contract still works. The regression trace tells you the game still works.

Let’s connect that to FireRed. In the research packet, the overworld source is represented by pokefirered/src/overworld.c, chunk_7ea6afd1253d6817, specifically around LoadMapFromWarp. That is a very important architectural phrase: load map from warp. We have talked all semester about maps as data-backed runtime spaces, not just images. A warp is not “move the screen.” A warp is a request to reconstruct the active map context from structured state: map group, map number, coordinates, connection behavior, object events, callbacks, and whatever else the overworld runtime must reestablish. So when our minimal clone implements map loading, we do not need all of FireRed’s complexity. But we do need the same shape of responsibility.

For the board, under “minimal map loader,” I’m writing four fields: map id, spawn position, events, and post-load callback. That is enough for a clone test. Not enough for a commercial RPG, not enough for full FireRed, but enough for a regression architecture. The loader should accept a saved or scripted location, build the active map state, install object events or triggers, and hand control to the overworld loop. If the player moves one tile and steps on a trigger, the map system should not directly mutate battle internals. It should ask the script system to execute a script.

That brings us to script execution. Minimal script execution does not mean “fake everything with direct calls.” It means build a tiny command interpreter with the same architectural role as the real one. The script can be as small as: lock player, display line, start battle, wait for battle result, set flag, release player. You do not need the full FireRed script command set to test the trace. You do need a persistent script context that can pause at battle start and resume after battle result. That pause-and-resume shape is essential.

Here is the concrete example. Suppose the player talks to a trainer. In your clone, the trainer object has script id “trainer_route1_bugcatcher.” The script begins, sets an interaction lock, calls StartTrainerBattle with trainer id 12, then yields. The battle engine runs. After victory, the battle result is written into a small result register, maybe battleResult equals WIN. Then the script resumes, checks that register, sets flag TRAINER_12_DEFEATED, awards money, removes the trainer’s battle availability, and returns control to the overworld.

Notice how many modules that single example touches. Map object event. Script VM. Trainer data. Party data. Battle setup. Battle turn execution. Reward logic. Event flags. Save persistence. That is exactly why it is a regression test.

Now let’s bring in the Showdown side again. In pokemon-showdown/test/common.js, chunk_979fde4c3177ea4f, the testing support around createBattle options and deterministic seed gives us an explicit model of seeded setup. For our purposes, the insight is that battle tests should make randomness either deterministic or irrelevant. Those are two different strategies.

I’ll say that again because it matters. Strategy one: deterministic randomness. You seed the RNG, then assert the exact expected sequence of outcomes. Strategy two: RNG-independent assertions. You make choices and assert properties that should hold regardless of the random branch. The source focus includes pokemon-showdown/test/TESTS.md, chunk_e8da8b9a442469fd, around makeChoices and RNG-independent assertions. That tells us a mature test suite does not always overfit to exact random rolls. Sometimes the better test says, “After this choice, the active Pokemon is no longer at full HP,” or “this Pokemon fainted,” or “the stat stage changed,” rather than “the HP must be exactly 37.”

Now, in FireRed-style architecture, this distinction is even more important because a single gameplay sequence may include multiple randomness consumers. Wild encounter selection, AI decision-making, damage rolls, secondary effects, critical hits, accuracy checks, and post-battle rewards can all draw from RNG. If your test asserts exact final HP after a long sequence without controlling every random consumer, the test may become brittle. It will fail not because the architecture regressed, but because you changed the order in which random numbers were consumed.

So what assertion vocabulary do we want? Showdown gives us examples. The research packet points us to pokemon-showdown/test/assert.js, chunk_26026d44764fa919, for assertions around species, fainted state, and full HP. It also points us to pokemon-showdown/test/assert.js, chunk_f2ab14d37f397660, for stat stage and HP-change assertions. Translate that into our clone architecture. We want assertions like “party slot 0 species is Bulbasaur,” “active battler is fainted,” “party slot 0 is not full HP,” “enemy attack stage equals minus one,” “inventory Potion count decreased by one,” “event flag trainer defeated is true,” and “save file reload preserves the changed HP.”

These are not glamorous assertions, but they are the language of confidence.

Let’s do a board-work pass on a minimal clone test. I’m going to write this as spoken pseudocode, not real code yet. First, create a SaveState with player location Route 1, coordinates 10, 5, party containing one level 5 creature at 12 out of 20 HP, inventory containing one Potion, and flags empty. Second, call LoadMapFromSave. Third, move the player to coordinate 10, 6. Fourth, execute the tile script there. Fifth, script starts a wild battle against a level 3 creature. Sixth, create battle with seed 12345. Seventh, choose item Potion on party slot 0. Eighth, assert HP is 20 out of 20 and Potion count is 0. Ninth, choose run or resolve victory, depending on the test’s purpose. Tenth, return to overworld. Eleventh, save. Twelfth, reload. Thirteenth, assert HP is still 20, Potion count is still 0, and map location is correct.

The advanced move here is that we are not testing every detail in one test. We are choosing one contract and letting the trace exercise the rest. This test’s main claim is “battle item use mutates party and inventory through the same state path that save persistence observes.” It is not trying to prove the entire damage formula. A different regression test can own damage. A different one can own fainting. A different one can own trainer victory flags.

Let’s check understanding again. What would be a bad assertion for this test? A bad assertion would be something like “the debug log contains exactly 47 lines,” unless the log format itself is the contract being tested. Another bad assertion would be “the enemy’s final HP is exactly this value” if enemy HP was affected by a random damage roll you did not control. A better assertion might be “the item count decreased,” “the selected party member healed up to the cap,” and “the post-save reload agrees with the in-memory state.”

Now, where does FireRed battle setup fit? The packet includes pokefirered/src/battle_setup.c, chunk_494d132a59422c85, with includes that tell a story all by themselves. That source touches task.h, overworld.h, item.h, pokemon.h, load_save.h, safari_zone.h, quest_log.h, script.h, event_data.h, event_object_movement.h, field control, battle.h, battle_transition.h, and more. I want you to notice the architecture signal there. Battle setup is not a pure battle module. It is a boundary module. It gathers context from overworld, scripts, event data, party, item, transition, and save-related systems. That is exactly the kind of module whose behavior should be protected by regression tests.

When a source file includes that many neighboring systems, you should not automatically say, “bad coupling.” In an engine like FireRed, some modules are supposed to be boundary modules. The real question is whether the boundary is explicit, testable, and recoverable. Battle setup has to know enough about the overworld to leave it and enough about battle to enter it. The test harness should model that boundary, not pretend it does not exist.

This is also where debug logs and replay output come in. For our clone, a replay log is not just a developer convenience. It is an architectural artifact. If a test fails, you want to know whether the map loaded incorrectly, the script skipped a yield, the battle started with the wrong party snapshot, the item command failed validation, the turn queue resolved in the wrong order, or the save layer serialized stale state. A compact replay log gives you that trace.

On the board, under “replay log,” I’m writing entries like: LOAD_SAVE slot 0, LOAD_MAP route1 at 10 5, MOVE_PLAYER south, SCRIPT_START wild_grass, BATTLE_START wild seed 12345, CHOICE item potion target party0, ITEM_APPLY potion heal 8, INVENTORY_DEC potion 1 to 0, BATTLE_END ran, RETURN_MAP route1 at 10 6, SAVE_COMMIT slot 0. That kind of log is not for the player. It is for us, and for future us. It says, “Here is the exact behavioral path this test covered.”

Now we have to be careful. A debug log can become another brittle output if we assert it too literally. The purpose of the log is usually diagnosis and replay, not line-for-line golden testing. A golden replay test can be valuable for a narrow engine, but it must be designed consciously. In most gameplay regression tests, the log is captured when the test fails and maybe compared at key milestones, not treated as sacred punctuation.

Let’s return to the save side. The packet points to pokefirered/src/save.c, chunk_9900b8674c89af66, described around save continuation. That phrase matters because save is not just a disk write at the end of a game. In a cartridge architecture, save has continuation behavior: enough state must be committed that the game can resume with coherent player position, party, inventory, flags, and system state. In our clone, save persistence should not be an afterthought tacked onto the test. It should be the final witness. If memory says the Potion is gone and HP is restored, but reload says otherwise, then your architecture did not commit the gameplay result.

So, minimal save persistence for the clone needs a few durable records: player location, party creatures, inventory quantities, event flags, and maybe script continuation state if you support saving mid-script. For today’s battle regression tests, I would initially avoid saving mid-battle or mid-script unless that is the exact feature under test. But I would absolutely test saving after battle and after returning to overworld, because that is the normal player-facing commitment point.

Let’s make this more sophisticated, because you are advanced learners and the final assessment is cumulative. You are not just being asked, “Can you write tests?” You are being asked to produce an architecture trace. That means every assertion should map back to a system responsibility. If your assertion says “flag TRAINER_12_DEFEATED is true,” you should be able to say: the script system owns the command that sets the flag, event_data owns the storage model, save owns persistence, and map object availability reads that flag when rebuilding the map. If your assertion says “party slot 0 HP is 20,” you should be able to say: party data owns the durable creature record, battle owns temporary battler state, item logic bridges inventory and party mutation, and post-battle commit reconciles battle state back into party state.

This is where many clone projects fail. They build a battle engine with its own Pokemon objects. They build a party screen with different Pokemon objects. They build save data with a third representation. Then they spend the rest of the project writing conversion glue and debugging lost state. FireRed, whatever complexity it has, keeps forcing us to ask where the authoritative state lives. Showdown, in its test design, keeps forcing us to ask whether the setup is deterministic and the assertions are meaningful. Week 24 is where those two lessons meet.

Now, small caution. When we design a “minimal clone version,” minimal does not mean unserious. It means deliberately reduced. A minimal map loader still has a map identity and events. A minimal script VM still has command sequencing and yield points. A minimal party model still distinguishes durable party data from temporary battle state. A minimal battle turn system still has choice validation and resolution order. A minimal item system still checks quantity, target legality, and effect cap. A minimal save system still serializes and reloads the state that gameplay mutated.

Let me give you a second example, this time trainer battle instead of item use. The test begins with a save where the player is in a town, has one creature, and trainer flag TUTORIAL_RIVAL_DEFEATED is false. Map load places a rival object near the exit. The player steps onto a trigger tile. Script locks input, moves the rival object toward the player, starts dialogue, starts battle, and yields. The battle is created with a deterministic seed. The player chooses Tackle; the rival chooses Growl. We assert that the player’s attack stage changes, using the kind of stat-stage assertion vocabulary represented by pokemon-showdown/test/assert.js, chunk_f2ab14d37f397660. Then we force or choose a victory path. The script resumes, sets the defeated flag, awards an item, releases input, and returns to overworld. Save commits. Reload confirms the rival no longer challenges the player.

That test is powerful because it protects a story event and a combat mechanic at the same time. If someone changes script resume behavior and the flag no longer sets, the test catches it. If someone changes battle stat stages and Growl no longer affects attack, the test catches it. If someone changes map reload and the rival appears as undefeated after reload, the test catches it. It is not merely a battle test. It is a full-system trace with battle as the stressor.

One more check for understanding before we move to the next layer. In that trainer test, what is the seed for? The seed is not there because we love seeds. The seed is there to make the battle reproducible. If Growl has no random component, and if we only assert the stat stage after Growl, the test may not need a seed for that assertion. But the battle harness as a whole should still accept one, because the next test may involve accuracy, critical hits, or secondary effects. The design lesson from pokemon-showdown/test/TESTS.md, chunk_134ac9f75b9d6477, is that controlled battle construction is part of the test API.

So as we continue, keep this sentence in mind: a battle regression test is a miniature game trace with a claim attached. The trace is the path. The claim is the assertion. If the trace is too small, you miss integration failures. If the claim is too broad, you get brittle tests. Our job is to choose the smallest trace that still crosses the architectural boundary we care about.

Now I’m going to pivot from vocabulary into structure, because the next question is: what modules do we actually implement in the clone so these tests can exist? We need just enough map loading, just enough script execution, just enough party data, just enough battle turns, just enough item use, and just enough save persistence. And the trick is that “just enough” has to preserve the real dependency direction. The map should not know how damage works. The battle engine should not know how to serialize the save file. The item system should not secretly reach around the party API if the party API is the durable owner. The script system should coordinate, not become a dumping ground for every game rule.

Hold that design pressure in your head, because next we are going to lay out the minimal interfaces and show how a single test drives them.

So, if I write this on the board, I want six boxes, not one giant “game” box. The boxes are map loader, script runner, party model, battle simulator, item service, and save repository. Now notice the names. I am deliberately not saying map manager, script manager, battle manager, item manager, because “manager” is usually where architecture goes to hide. For the clone, each box needs a narrow verb. The map loader loads a map from a location or warp. The script runner advances a script against game state. The party model owns creatures and exposes legal mutations. The battle simulator resolves turns from explicit inputs. The item service checks and applies item effects. The save repository snapshots and restores durable state.

That is already a regression-testing architecture. Why? Because each verb can become an executable sentence. Load this save. Load this map through this warp. Step onto this tile. Run this script until it requests battle. Create a battle with this party and this seed. Make these choices. Assert HP, fainting, item counts, flags, and save continuation. That sentence is not documentation beside the system; that sentence is the system being held to account.

Now, FireRed itself is not arranged in exactly these clean little teaching boxes, of course. It is a production GBA codebase with globals, callbacks, tasks, and a lot of compile-time data. But the dependency direction is visible. In pokefirered/src/overworld.c, chunk_7ea6afd1253d6817, the LoadMapFromWarp path belongs to the world transition responsibility. It is about taking a warp context and making the new map state current. That is not where move damage belongs. Likewise, pokefirered/src/save.c, chunk_9900b8674c89af66, supports the continuation story: save data is what allows the game to resume with persistent state, not a place where we should bury battle resolution logic. And when battle setup needs to bridge the field into combat, pokefirered/src/battle_setup.c, chunk_494d132a59422c85, is important because it includes the connective tissue: overworld, item, script, event data, battle, battle transition. It reminds us that bridge modules exist, but bridges are not the same thing as letting every city melt into every other city.

Let me make that concrete. Suppose our final lab sequence begins from a saved player standing in front of a trainer. The player presses up. The overworld movement code decides the player has entered a trigger tile, or maybe an object event sees the player. A script begins. The script says, in effect, show text, start trainer battle, set a defeated flag, resume overworld. In the clone, the script runner should not calculate Thunderbolt damage. It should emit or invoke a battle request. The battle simulator should not decide which flag marks that trainer defeated. It should return a battle result. Then the script runner, still owning progression context, can set the flag and award money or items through the appropriate services.

Let’s put a small interface on the board. I’ll say it out loud rather than write syntax too carefully. MapLoader has loadFromWarp(saveState, warpId), and it returns a map session: map id, layout, object events, script triggers, player spawn. ScriptRunner has stepUntilBlocked(gameState, scriptId), and blocked means waiting for input, waiting for battle, waiting for message dismissal, or finished. Party has getActive, applyDamage, heal, awardExp, and maybe replaceFaintedActive, but each of those goes through creature identity. Battle has createBattle(battleSetup, seed), choose(side, action), and resolveTurn. Items have canUseItem(context, itemId, target) and applyItem(context, itemId, target). Save has load(slot) and persist(gameState). That is enough. It is intentionally unglamorous.

Here is the important testing move. We do not test this by saying, “After the trainer battle, the game works.” That is too mushy. We test by making each cross-boundary fact observable. Showdown gives us the comparison vocabulary for that. In pokemon-showdown/test/TESTS.md, chunk_134ac9f75b9d6477, createBattle, explicit seeds, and forceRandomChance are presented as test helpers for making battle setup deterministic. In pokemon-showdown/test/common.js, chunk_979fde4c3177ea4f, createBattle options and deterministic seed support the same lesson: a battle test should be able to say what battle exists, what randomness is permitted, and what state should follow. Then pokemon-showdown/test/assert.js, chunk_26026d44764fa919, gives us assertion vocabulary around species, fainted state, and full HP, while chunk_f2ab14d37f397660 extends that vocabulary into stat stages and HP-change assertions. We are borrowing that testing posture, not replacing FireRed with Showdown.

Now, check your understanding here. If the test says, “after the battle, the trainer flag is set,” which module should probably make that mutation? Not the battle simulator. The battle simulator can say, “player won,” “enemy fainted,” “turn count was two,” “participant gained experience,” depending on how much we model. But the defeated-trainer flag is progression state. That belongs with the script or event-data layer. If your battle simulator imports the map script table so it can set a trainer’s local flag directly, you have made the final test easier today and the system harder to verify tomorrow.

Let’s walk through a minimal clone sequence. Imagine a saved game state with player map equal to Route 3, x equals 10, y equals 4, facing north, one potion in the bag, and a party with a level 5 Charmander and a level 3 Pidgey. I am using familiar species names only because they make the state easy to visualize; the architectural point is independent of those particular records. There is a trainer object at x 10, y 2, with a sight range. The player moves north from y 4 to y 3. The map session says the trainer can see the player. The script runner starts trainer_script_27. The script runner emits a message event, then a battle request containing the opponent party and battle type. The test now pauses the overworld side and creates a battle.

At battle creation time we need to freeze every source of variability that would otherwise make the test flaky. In Showdown language, this is where createBattle and seeded randomness matter. Again, pokemon-showdown/test/TESTS.md, chunk_134ac9f75b9d6477, is useful because it shows tests using seed control and random forcing so that chance-based behavior can be specified. In our clone, maybe the first turn is Scratch into Tackle, no critical hits, no misses. We can either choose moves with no randomness or provide the seed and assert outcomes that are independent of exact random rolls. And that second style matters. Pokemon-showdown/test/TESTS.md, chunk_e8da8b9a442469fd, points us toward makeChoices and RNG-independent assertions. The advanced move is not always to control every random bit. Sometimes the better test asserts invariants: HP decreased by some legal range, the active creature did not faint, the item count changed exactly once, the battle log contains the expected decision boundary, and the battle remains requestable for the next turn.

Now, in a final architecture trace, we probably want both styles. For the first test, use deterministic seed and exact HP values, because you are proving the clone’s basic battle arithmetic is wired correctly. For the second test, use RNG-independent assertions, because you are proving the source map from data to behavior remains stable even if the battle formula later adds critical hits or variance. This is one of those places where advanced testing is less brittle than beginner testing. Beginner tests often assert every pixel of state. Advanced tests assert the contract at the right boundary.

Let me pause and ask: what is the boundary contract after one battle turn? You should be able to answer in nouns. Active combatants, HP, status, volatile turn state if modeled, consumed choices, battle log, pending request, and maybe side conditions. You should not answer “the whole game.” The whole game is too much for one boundary. After the battle ends, then we cross back to game state: party HP updated, opponent defeated, reward applied, flags updated, script pointer advanced, map control returned, save dirty bit set. Different contract.

This is why I want the clone party model to be boring and explicit. The party should not be an array that everyone mutates directly in tests. It should be inspectable, yes, but mutation should go through named operations. Apply damage. Heal by item. Mark fainted. Award experience. Add creature. Remove item from held item slot if you model that. Remember the summary screen source from FireRed, pokefirered/src/pokemon_summary_screen.c, chunk_e8e17a42bbf17d46, includes party, item, data, battle, moves, and UI-related headers. That tells us the real game has many consumers of creature state. Many systems need to display or reference a Pokemon, but that does not mean every system should own the rules for mutating one. For the clone, this is where we can be cleaner than the original environment while still preserving the original dependency shape.

Now item use. Item use is the place where many student clone architectures accidentally collapse. A potion feels simple: subtract one potion, add 20 HP. So the script just does it, or the battle turn just does it, or the bag menu just does it. But then a potion in battle has timing rules, target rules, inventory rules, and turn-cost rules. A potion outside battle has different permissions. A key item may run a script. A held item may be consumed by battle logic. So the item service needs context. “Can use Potion on party slot zero in battle?” is not the same question as “Can use Potion on party slot zero in overworld?” The service should answer legality and apply the durable mutation, while the battle simulator accounts for the action cost if the item was used as a battle choice.

Here is our concrete test variant. The player enters battle with Charmander at 8 out of 20 HP and one potion in inventory. Turn one, the player uses Potion, the enemy uses Tackle. With a fixed seed, we assert that the potion count goes from one to zero, Charmander HP increases before or after damage according to our chosen turn-order rules, and the battle log records the item use and the attack. If this is a FireRed-inspired clone, we would care about the actual cartridge’s item timing eventually. But at this stage, the course objective is architectural: the item count mutation should be visible in game state, the HP mutation should go through party or battle-owned combatant state, and the test should say which state is authoritative when the battle returns to overworld.

This gets us to a subtle but important question: does battle own a copy of the party, or does it mutate the party directly? FireRed’s actual engine has its own battle structs and buffers, and then synchronizes outcomes back into persistent party data. For our minimal clone, I prefer battle receives a battle-facing projection of the party and returns a result patch. The party model then applies that patch. Why? Because it makes tests sharper. You can assert that battle resolution did not accidentally set map flags. You can assert that applying the battle result changes party HP and experience in exactly one place. And you can replay the battle log independently from save persistence.

This is where debug logs become more than print statements. A proper debug log is a replay artifact. It should include the seed, initial battle setup, player choices, enemy choices if chosen explicitly, random decisions, major state deltas, and final result. Showdown’s tests are useful here because makeChoices is not just an API convenience; it is a representation of player intent entering the simulator. We covered request state and commit flow previously, so I will not re-teach that loop. I just want you to see how it becomes a regression artifact in week 24. A log line like “turn 1: p1 move Scratch, p2 move Tackle, random crit false, damage 5” lets a future failing test become understandable. Without it, the test says only “expected 13, got 12,” and now you are spelunking.

Let’s return to map loading and save persistence. Load save, enter map, move player, trigger script, start battle, resolve a turn, award result, update flags/items/party, return to overworld, persist state. That is the workshop sequence. The source map for that sequence should point to FireRed first. pokefirered/src/overworld.c, chunk_7ea6afd1253d6817, supports the map-from-warp portion. pokefirered/src/battle_setup.c, chunk_494d132a59422c85, supports the bridge from field context into battle. pokefirered/src/save.c, chunk_9900b8674c89af66, supports the continuation and persistence portion. The Showdown test files then give us the regression-testing grammar for the battle middle: createBattle and seeds in pokemon-showdown/test/common.js, chunk_979fde4c3177ea4f, choice execution from pokemon-showdown/test/TESTS.md, chunk_e8da8b9a442469fd, and assertion vocabulary from pokemon-showdown/test/assert.js, chunks chunk_26026d44764fa919 and chunk_f2ab14d37f397660.

Now watch what happens if we make this an end-to-end test without losing modularity. The test begins by loading save slot A. It asserts current map and player position. It calls mapLoader.loadFromWarp or loadFromSavePosition, depending on whether we model boot continuation separately. It advances one movement command. It asserts a script is now active. It runs the script until battle request. It creates battle with seed 12345. It makes choices for one or more turns. It asserts combat state using battle assertions: species, HP, fainted, stat stage if relevant. It completes battle. It hands battle result back to script runner. It asserts trainer flag set, reward applied, inventory changed if item was used, party HP updated. It returns to overworld. It persists. Then, and this is the part many people skip, it reloads from save and asserts the same durable state is present.

That final reload is what separates a gameplay test from a persistence test. If you only inspect memory before saving, you have not tested the save boundary. And since FireRed’s course arc has repeatedly emphasized data table to runtime globals to callbacks and tasks to behavior, week 24 adds one more clause: behavior back to durable data. The loop is not complete until state can survive continuation.

Check yourself on this design. If a test fails after reload because the trainer flag is missing, where do you look first? Not battle damage. Not map layout. You look at the handoff from script progression to event flags, then the save serialization of those flags, then the load reconstruction. If HP is correct before save but wrong after reload, you look at party persistence. If HP is wrong immediately after the turn, you look at battle result application or item timing. The value of this architecture is not that bugs disappear. The value is that a failing assertion points toward the correct subsystem.

Let me add one more advanced nuance before we take the next step. Minimal clones should model time at the boundary where the real system has time. In FireRed, the runtime shell uses callbacks and tasks; earlier in the course we traced AgbMain, callback slots, SetMainCallback2, task creation, and RunTasks. We are not going to rebuild that whole shell in the final lab. But we should preserve the idea that overworld control, script execution, message boxes, transitions, and battles are staged processes, not one giant synchronous function called playGameToEnd. In the clone, “step until blocked” is a good compromise. It lets the script runner behave like a small state machine. It gives tests control. And it still resembles the callback/task discipline enough that our architecture trace remains honest.

So if I draw the board version now, the arrows go like this. Save repository produces game state. Map loader reads game state and map data to create a map session. Movement updates player position through overworld rules. Script runner observes triggers and coordinates progression. Battle setup builds a battle request. Battle simulator resolves choices under a seed and returns a battle result. Party and item services apply legal durable mutations. Script runner updates flags and returns control. Save repository persists the new game state. The arrows do not all point both ways. That is the main lesson.

And this connects week 24 to the whole course plan. In week 1, we treated the game as cooperating runtime systems rather than a pile of screens. In the data weeks, we learned that species, moves, items, maps, and scripts are records that runtime code interprets. In the callback and task weeks, we learned that behavior is scheduled and staged. In the FireRed tracing weeks, we followed concrete paths through headers, globals, scripts, structs, and source functions. In the Showdown comparison weeks, we used Dex lookup and battle simulator APIs as a secondary reference for clean data access and deterministic combat tests. Now we are cashing all of that out as executable architecture. The final trace is not a diagram you admire; it is a test that can fail.

For the next part, we are going to write the test sentence in a more exact form. Not code yet, but almost code. I want you to listen for the assertion vocabulary. Every time we cross a subsystem boundary, we will ask, “What can we assert here that proves the boundary contract without overfitting to internals?” That question is the difference between a regression suite that protects architecture and a regression suite that merely preserves accidents.

So, if I write our near-code sentence on the board, it sounds like this: given a known save state, when the player loads into a known map, takes a known movement path, triggers a known script, enters a known battle with a known party and deterministic random seed, chooses a known action sequence, uses a known item, resolves a known battle result, returns to the overworld, and saves, then the resulting state must match the expected map position, flags, inventory counts, party data, battle outcome, and persisted save continuation.

Pause on that for a moment. That is a long sentence, but it is not vague. It is long because the game is long. A creature RPG is not one algorithm. It is a chain of contracts. And by week 24, the level of the course has changed. Earlier in the semester, I would have accepted, “the battle system should damage the opponent,” as a reasonable student explanation. Now that is too weak. We need to know where the battle came from, which data initialized it, which party objects entered it, which input choices were accepted, how randomness was pinned down, which flags or items changed afterward, and whether the save layer preserved the result.

Now, notice something important. This does not mean the test should assert every byte of memory. That is the temptation, especially if you come from emulator testing or snapshot testing. You say, “Well, if I snapshot all of RAM before and after, I will catch everything.” You will catch everything, yes, including things you should not care about. You will catch timer drift. You will catch animation state. You will catch incidental task ordering. You will catch data layout details that change when a harmless refactor moves a temporary field. A good regression test is not a museum case for the whole machine. It is a contract probe.

Let me say that again because it is the central idea of this lecture segment: the test should probe the architectural boundary, not worship the current implementation. In Showdown’s test system, the reason the test helper design is useful to us is not that it is the same engine as FireRed. It is not. The useful part is that it gives us a model of battle as a deterministic fixture. The source focus for today points us to pokemon-showdown/test/TESTS.md, especially chunk_134ac9f75b9d6477, where createBattle, seeds, and forceRandomChance are part of the testing vocabulary. The lesson is that a battle regression test begins by constructing the battle world explicitly. It does not ask the global server state politely for whatever happens to exist. It says, “Here are the players, here are the teams, here is the seed, here is the forced random outcome when needed.”

And then, in pokemon-showdown/test/common.js, chunk_979fde4c3177ea4f, the createBattle options and deterministic seed become a reusable test mechanism rather than a one-off hack. That matters architecturally. Determinism is not just a convenience for tests. Determinism is a way of making the battle engine accountable. If you cannot replay a turn, you cannot tell whether a bug belongs to move resolution, speed ordering, random chance, item effects, or post-turn cleanup.

Now let us translate that back to our FireRed clone architecture. Imagine our minimal clone has six modules. Map loading, script execution, party data, battle turns, item use, and save persistence. I am intentionally using minimal clone versions here, not the full decompilation. That is one of the objectives for today. If you cannot create a small version of the architecture, you probably do not yet understand the large version. The clone does not need every move, every map, every item, every event object, or every save block detail. It needs the boundary shape.

On the board, I am going to draw six boxes in a line. First box: save. Second box: map. Third: script. Fourth: party. Fifth: battle. Sixth: persistence. Then I am going to draw a little loop from battle back to script, because battle results usually feed progression logic. Then another line from script to inventory and party, because scripts often give or remove items, set flags, heal party members, or start encounters. Then a line from overworld to save, because the player returns to the map and persists the new state.

For a minimal clone, the save loader does not need to emulate flash memory timing. It needs to produce a game state object with stable fields. Player location. Current map id. Facing direction. Event flags. Inventory. Party. Maybe money, if the battle reward touches it. The important connection to FireRed is that save continuation is a real architectural concern, not just a menu feature. The source packet points us to pokefirered/src/save.c, chunk_9900b8674c89af66, for save continuation. We do not need to re-teach the full save system here, but we do need to preserve the idea that loading a saved game is not “start a new scene.” It is reconstructing enough state for the overworld runtime to continue coherently.

Now, map loading. In FireRed, map entry is not a decorative load screen. The map loader establishes the local world in which scripts, objects, warps, and movement now make sense. The source focus gives us pokefirered/src/overworld.c, chunk_7ea6afd1253d6817, for LoadMapFromWarp. The name itself is valuable. Load map from warp. That phrase tells us that the map load is parameterized by a transition point, not just by a map name. A warp includes where you come out, not merely where you are going. So in the minimal clone, the map loader should accept a source state that says, “Load map PalletTown, entry point 2, player position X and Y, facing down,” or whatever your fixture uses. The assertion after map loading should not be “all map internals equal this giant blob.” The assertion should be, “current map id is this, player coordinates are this, active object set includes the script trigger we expect, and the collision or trigger layer makes the next movement meaningful.”

Let me give you a concrete example. Suppose our final architecture trace begins with a save in which the player is standing just outside a trainer’s sight line. The map is Route Test 01. The party contains one level 5 creature with 20 out of 20 HP, move Tackle, and maybe a Potion in the bag. A trainer event flag is unset, meaning the trainer has not been defeated. The test inputs are: load save, move one tile north, trigger trainer script, start battle, choose Tackle, enemy chooses Tackle, use Potion on turn two, choose Tackle again, win, set defeated flag, return to map, save.

The map loading assertion after the first step is not about battle. It is about map readiness. “The current map is Route Test 01. The player is at coordinate 10, 14. The trainer object exists at coordinate 10, 10. The trainer defeated flag is false. The next movement north will enter the trainer’s detection region.” That is a clean boundary assertion.

Then we move to script execution. The script executor is a very powerful part of a FireRed-style RPG because it is where data-driven world behavior becomes imperative control flow. Earlier in the course, we traced scripts through globals and callbacks and tasks, so I am not going to re-teach that machinery. I only want to carry forward the idea that a script is not just dialogue. It is a control program for progression. It can show text, set flags, give items, start battles, warp the player, or call special functions. The minimal clone version can be tiny. It can have opcodes like showMessage, checkFlag, setFlag, startBattle, giveItem, and returnToOverworld. That is enough to test the architecture.

What do we assert at the script boundary? We assert that the right script started, that it received the right game state, and that it requested the right transition. Not every text box timing detail. Not every frame of the exclamation mark animation. If this is a regression test for architecture, then after movement we might assert, “active script id equals TrainerYoungsterAChallenge,” and after the first script segment we might assert, “battle request created with trainer id YoungsterA, party source trainer table, battle type trainer, post-battle continuation script TrainerYoungsterADefeated.” In FireRed, the battle setup layer sits between overworld events and battle. The research packet’s pokefirered/src/battle_setup.c, chunk_494d132a59422c85, is a good supporting source here because the include surface alone shows how battle setup is not isolated from the field. It includes overworld, item, script, event data, event object movement, field map, player avatar, battle, battle transition, and related constants. That is the point. Battle setup is a bridge module.

Now, let us check understanding. If I ask, “Why should the script test assert a battle request instead of immediately asserting enemy HP,” what is the answer? Think it through. Because at that boundary, the script has not resolved a battle turn. The script’s job is to identify that a battle should begin, with the right participants and the right continuation. Enemy HP belongs to the battle engine boundary. If the script test asserts enemy HP directly, it is crossing layers and making the failure harder to diagnose. If that assertion fails, was it the trainer trigger? The battle initialization? The move formula? The random seed? The post-turn effect? You do not know. Boundary-specific assertions preserve diagnostic clarity.

Now we enter party data. The party model is where your clone needs just enough biological persistence to make battle and save meaningful. Species, level, current HP, max HP, moves, maybe status, experience, held item if your trace needs it. We have already used Dex lookup and species data as a prior foundation during the Showdown comparison weeks, so we do not need to reopen the whole data facade discussion. What matters today is that party data is both input and output. The party entering battle is not a disposable battle sprite. It is persistent game state temporarily represented inside combat.

This is where Showdown’s assertion vocabulary becomes useful as a comparison. In pokemon-showdown/test/assert.js, chunk_26026d44764fa919, the test vocabulary includes assertions around species, fainted, and fullHP. In chunk_f2ab14d37f397660, it includes statStage and HP-change assertions. The important architectural lesson is that assertions should speak the language of combat state. “This Pokémon fainted.” “This Pokémon is at full HP.” “This stat stage changed.” “This HP change occurred.” That is much better than asserting that some internal array index now contains 37. The vocabulary should express game facts.

For our minimal FireRed-style clone, I would give the test helper names like assertPartySpecies, assertPartyHP, assertNotFainted, assertItemCount, assertFlagSet, assertMapPosition, assertBattleWinner, and assertSaveRoundTrips. Notice how those helpers line up with subsystem contracts. Party. Inventory. Event data. Map. Battle. Save. A helper is not just syntactic sugar. A helper is a teaching device for the architecture. It says, “This is the level at which this system promises stability.”

Now let us make the battle turn deterministic. We do not need to re-teach Showdown’s action queue or player choice commit flow from last week. We do need to use them. In pokemon-showdown/test/TESTS.md, chunk_e8da8b9a442469fd, makeChoices and RNG-independent assertions are emphasized in the source focus. That gives us two lessons. First, player actions should be expressed as choices, not as direct mutation of battle internals. Second, tests should avoid depending on randomness unless randomness is the subject of the test. If you are testing Potion use, do not let a critical hit decide whether the Potion was necessary. If you are testing a trainer defeat flag, do not let a miss chance decide whether the battle ended.

So in the clone test, we seed the battle or force relevant random outcomes. If Tackle has no miss chance in your simplified rules, fine. If it does, force hit. If damage variance exists, fix the seed or use an assertion range only when the rule allows a range. But be careful with ranges. “HP is between 7 and 11” may be appropriate if the mechanic is variable damage and the test is intentionally broad. But if the lecture objective is executable architecture, then a fixed seed is usually clearer. The test says, “with seed 12345, after turn one, player HP is 14 and enemy HP is 8.” That is precise, replayable, and debuggable.

Now, item use. Item use is deceptively interesting because it crosses more boundaries than students expect. A Potion is not just “add HP.” In a battle context, using a Potion is a player choice, an inventory decrement, a target validation, a healing effect, and a turn action. In an overworld context, using a Potion might be menu-driven and not consume a battle turn. So the same item data can participate in different runtime contexts. A minimal clone test should make this explicit.

Let us say the player has one Potion and the active creature has 6 out of 20 HP after the enemy attacks. On turn two, the player chooses item Potion targeting party slot zero. The battle engine validates that the item exists, that it is usable in battle, that the target is a living party member, and that HP is below maximum. Then it applies healing up to max HP and decrements inventory. What do we assert? We assert that inventory count goes from one to zero. We assert that HP increases by the expected amount, capped at max. We assert that the turn continues or passes according to the battle rule we have chosen for the clone. And if the enemy also acts after item use, we assert the post-turn HP after both the heal and the enemy move, not halfway through, unless the test is specifically checking mid-turn sequencing.

This is a good place for a board-work cue. I am writing two columns. Left column: “observable contract.” Right column: “implementation accident.” Under observable contract, I write: item count decreased, target HP healed, battle turn consumed, invalid target rejected, fainted target rule respected if your design has one. Under implementation accident, I write: item effect function was called exactly once, internal queue length was three, temporary variable healAmount existed, menu cursor index was two. Some of those internal facts may be useful for unit tests inside the module, but they are not the right assertions for the end-to-end architecture trace.

And this is how we prevent the final week from becoming a giant brittle integration test. The lab this week asks you to produce a final architecture trace for one gameplay sequence: load save, enter map, move player, trigger script, start battle, resolve a turn, award result, update flags, items, party, return to overworld, and persist state. That trace should have checkpoints. It should not be one enormous assertion at the end that says “game state equals expected object.” A final equality assertion can be useful, but if it is the only assertion, it is a terrible teacher. You want the trace to say where the architecture changed hands.

Now let us talk about debug logs and replay output, because this is where advanced students can raise the quality of their engine dramatically. A deterministic battle test is much stronger when it produces a readable replay. Showdown’s ecosystem has this idea built deeply into its battle culture: choices go in, log lines come out, and the replay can be inspected. We are not copying Showdown’s protocol, but we are borrowing the principle. Your clone can emit a compact trace like: loadSave TestSaveA; loadMap RouteTest01 at 10,14; move north to 10,13; trigger TrainerYoungsterA; startBattle seed 12345; turn 1 player move Tackle enemy move Tackle; damage enemy minus 12 player minus 6; turn 2 player item Potion; heal player plus 14; enemy move Tackle; player minus 6; turn 3 player move Tackle; enemy fainted; battleWin; setFlag TrainerYoungsterA defeated; returnMap RouteTest01 at 10,13; save slot A.

That log is not just for humans. It is a source map from behavior back to systems. When a regression occurs, the log tells us whether the failure appeared before battle, during battle, after battle, or during persistence. This connects directly to the week’s theme: deterministic tests and full FireRed system trace. The phrase “source map” here is deliberate. Over the whole course, we have practiced tracing data tables to runtime globals, callbacks, tasks, scripts, structs, and source functions. Now the replay log is the runtime counterpart of that source trace.

Let me ask another check-for-understanding question. Suppose your end-to-end test fails because, after saving and reloading, the trainer defeated flag is false again. Which subsystem is most suspicious? Do not shout “script” too quickly. If the flag was true immediately after battle but false after reload, the script probably did its job. The suspicious boundary is save persistence, or the mapping between event flags and save data. If the flag was never true after battle, then script continuation or battle result handling is suspicious. If the battle never ended, battle resolution is suspicious. If the battle never began, script trigger or battle setup is suspicious. This is why staged assertions matter. They turn one failure into a diagnosis.

Now, in FireRed terms, returning to the overworld after battle is not merely “close battle screen.” It means the field runtime resumes with updated state. Battle setup and transition code, represented in our packet by pokefirered/src/battle_setup.c, chunk_494d132a59422c85, exists because the engine must cross from field to battle and back with continuity. The player should be on the correct map. The trainer should not challenge again if defeated. The party should carry damage or experience changes. Items used should remain consumed. Scripts may continue after battle to display text or set flags. If you have ever seen a bug where a trainer repeats the pre-battle dialogue after losing, or the map reloads with the player in the wrong position, you have seen a boundary contract failure.

For the minimal clone, we can express post-battle continuation with a script continuation token. When the script starts the battle, it stores something like “after battle, resume at label defeated.” The battle engine does not need to know the dialogue. It only returns a result: player won, player lost, fled, draw, or whatever your rules support. The script engine receives the result and continues. It sets the defeated flag and maybe gives money. Then it yields control back to overworld. The assertion vocabulary here should include assertFlagSet, assertScriptComplete, assertControlModeOverworld, and assertPlayerCanMove. That last one sounds small, but it is important. If control mode remains stuck in script or battle, the game is visibly broken even if the data looks correct.

Finally, save persistence. A save test is not complete when you call save. A save test is complete when you load what you saved and prove the relevant facts survived. That is why I like the phrase “round trip.” Save, clear runtime state if your harness allows it, reload, assert. The source focus on pokefirered/src/save.c, chunk_9900b8674c89af66, supports this idea of continuation. A save is a promise that the player can leave the process and return to a coherent world. In your clone, the persistence layer can be an in-memory serialized object, a JSON file, or a binary block. For this architecture exercise, the format is less important than the contract. Does map identity persist? Does player position persist? Do flags persist? Does inventory persist? Does party HP persist? Does battle not accidentally persist as active unless you designed mid-battle saves? FireRed-style games generally care deeply about where continuation is legal and what state is reconstructed.

Let me pull this together in one more concrete test narrative. The test begins with SaveFixture.TrainerPotion. It loads into RouteTest01, with player party slot zero containing Flameling, level 5, 20 of 20 HP, and one Potion in the bag. The map loader places the player at 10,14. The movement system applies input north. The trainer sight script triggers. The script creates a trainer battle request. The battle factory initializes combat with seed 12345. Turn one: player chooses Tackle, enemy chooses Tackle. We assert enemy HP decreased, player HP decreased, neither fainted. Turn two: player chooses Potion on party slot zero. We assert the Potion count becomes zero and HP is restored according to the item rule, then enemy damage applies. Turn three: player chooses Tackle and enemy faints. We assert battle winner is player, enemy fainted is true, player party survives with the expected HP. The script continuation sets TrainerYoungsterA defeated. We assert that flag. Control returns to overworld. We assert current map and position. Then save. Then reload. We assert the same map, same position, same defeated flag, same zero Potion count, and same party HP.

That is an executable specification of a vertical slice. It is not the whole game. It is better than the whole game for our purpose because it is small enough to understand and broad enough to cross every major boundary we care about this week.

So, as we continue, keep the distinction sharp. Showdown gives us a comparison point for deterministic battle setup, choice submission, seeded or forced randomness, and expressive combat assertions, as seen in pokemon-showdown/test/TESTS.md chunk_134ac9f75b9d6477, pokemon-showdown/test/TESTS.md chunk_e8da8b9a442469fd, pokemon-showdown/test/common.js chunk_979fde4c3177ea4f, and pokemon-showdown/test/assert.js chunks chunk_26026d44764fa919 and chunk_f2ab14d37f397660. FireRed gives us the architectural source path for overworld loading, battle setup, and save continuation, with pokefirered/src/overworld.c chunk_7ea6afd1253d6817, pokefirered/src/battle_setup.c chunk_494d132a59422c85, and pokefirered/src/save.c chunk_9900b8674c89af66. Our job is not to confuse those two projects. Our job is to use the comparison to become more precise about FireRed-style architecture.

Now, for the next stretch, I want to move from the spoken test sentence to the shape of the harness itself. We are going to ask what the fake engine must expose, what it must hide, and how to keep the test deterministic without flattening the architecture into one giant god object.

So, on the board, I am going to draw this as six boxes, but I want you to hear the important constraint before you see the boxes. The harness is allowed to be small, but it is not allowed to lie about ownership. That is the rule. Small is good. Fake is acceptable. Lying about ownership is where tests become misleading.

Here are the six boxes: map loader, script runner, party model, battle turn executor, item operation, save store. Now, notice what I did not draw. I did not draw “game.” I did not draw “player.” I did not draw “do everything.” Because the moment your regression harness has one object called Game that directly mutates the map, the party, the battle result, the inventory, and the save file, you no longer have a test of the architecture. You have a test of a shortcut.

In FireRed terms, that shortcut would erase the thing we spent the entire course learning how to see. When pokefirered/src/overworld.c chunk_7ea6afd1253d6817 gives us LoadMapFromWarp as an overworld-side operation, that matters architecturally. Loading a map is not just setting a string called currentMap. It is a transition through map header selection, warp context, object and layout state, and the callback world that will later run field behavior. And when pokefirered/src/battle_setup.c chunk_494d132a59422c85 pulls together headers for task, overworld, item, script, event data, battle, and battle transition, that is not random include noise. It is evidence that battle entry is a boundary-crossing operation. It has to coordinate systems without pretending they are one system.

So our minimal clone needs to preserve that shape. It can reduce detail, but it cannot collapse ownership. Let me say that another way. We do not need every metatile behavior, every trainer flag, every animation, every exact item pocket, or every save block checksum in the clone. But we do need the map loader to own map entry state, the script runner to own event progression, the party model to own creature state, the battle executor to own turn resolution, the item operation to own inventory mutation, and the save store to own persistence. That is the difference between a useful architecture test and a unit test with costumes.

Now let’s make this concrete. Imagine the gameplay sequence for the final workshop. The player loads a save in Viridian Forest, walks one tile, triggers a bug catcher script, starts a battle, uses Tackle, takes damage, wins, receives money or a flag update, returns to the overworld, uses a Potion afterward, and saves. In the production FireRed-shaped world, that crosses many real modules. In our clone, we want the same architectural itinerary with smaller data.

So the test sentence should sound like this: given a saved state with map id ForestA, player position x ten y five, party containing a level seven Charmander-like creature at twenty-two of twenty-four HP, bag containing one Potion, and event flag BugCatcher3Defeated false, when the save is loaded, the map is entered through a warp, the player steps onto trigger coordinate ten six, the script starts a trainer battle, the first battle turn resolves with deterministic damage, the enemy faints on turn two, the flag is set, the player returns to the field, uses a Potion, and saves, then the persisted state records the new map position, the battle flag, the changed party HP, and the decremented item count.

That is a mouthful, yes. But it is a very good mouthful, because every clause points to a boundary. “Save is loaded” belongs to persistence. “Map is entered through a warp” belongs to map loading, and we can connect that to the source role of LoadMapFromWarp in pokefirered/src/overworld.c chunk_7ea6afd1253d6817. “Script starts a trainer battle” belongs to script execution and battle setup, and the cross-system nature is supported by pokefirered/src/battle_setup.c chunk_494d132a59422c85. “Battle turn resolves with deterministic damage” belongs to the battle executor, and here our comparison point is Showdown’s test culture: pokemon-showdown/test/TESTS.md chunk_134ac9f75b9d6477 discusses createBattle, seeds, and forceRandomChance, while pokemon-showdown/test/common.js chunk_979fde4c3177ea4f anchors deterministic createBattle options and seed setup. “Assertions describe combat state” belongs to the assertion vocabulary, where pokemon-showdown/test/assert.js chunk_26026d44764fa919 and chunk_f2ab14d37f397660 give us examples of expressive checks for species, fainted status, full HP, stat stages, and HP changes.

Check for understanding here. If I ask you, “Where should the flag BugCatcher3Defeated be set?” the answer is not “wherever the test happens to notice victory.” The battle executor may emit a battle result. The script runner or event progression layer should consume that result and perform the event mutation. Why? Because the flag is not a battle mechanic. It is world progression state. That distinction is small in a toy harness and enormous in a real architecture.

Let me write a little pseudo-board trace verbally. Board left side: SaveStore.load returns SaveSnapshot. SaveSnapshot contains mapRef, playerPos, partySnapshot, bagSnapshot, flags. Arrow to MapRuntime.enterFromWarp. Arrow to ScriptRuntime.evaluateStepTrigger. Arrow to BattleSetup.createEncounter. Arrow to BattleRuntime.resolveTurn. Arrow back to ScriptRuntime.completeBattleBranch. Arrow to FieldRuntime.resume. Arrow to ItemRuntime.usePotion. Arrow to SaveStore.persist.

Now, what makes this a regression test instead of just a demo? Assertions at each important boundary. Not every line. Not every helper. Boundary assertions. After load, assert that the party and flags match the save snapshot. After map entry, assert that the active map id and player position match the warp destination. After the trigger, assert that a script is active or that the script emitted a battle-start command. After battle setup, assert the combatants and deterministic seed. After the turn, assert HP deltas or fainting state. After the result, assert event flag mutation and inventory or reward mutation. After save, assert that persisted state is the new authoritative state.

And now we need to be careful, because advanced students often overcorrect here. You hear “assert at boundaries,” and you write fifty assertions after every frame. That is usually not the goal. Showdown’s test style gives us a useful comparison. In pokemon-showdown/test/TESTS.md chunk_e8da8b9a442469fd, the emphasis around makeChoices and RNG-independent assertions is not “inspect every internal temporary.” It is “drive the simulator through public choice submission, then assert stable outcomes.” That is exactly the attitude we want. If a battle turn has a queue internally, and we already covered action queue resolution in the prior lecture, we do not need this final week to re-teach that queue by asserting every queue entry. We need to assert the observable contract that matters for the sequence.

So if Tackle damage is deterministic under seed 1234, you might assert that the defender’s HP changed by the expected amount. Or, better in some cases, assert that HP decreased and the final fainted state appears after the second move, especially if your clone does not model exact production damage. That distinction is important. If your clone claims to model exact FireRed damage, assert exact values. If your clone is an architectural trace, assert semantically stable values: battle started, party member participated, HP moved in the expected direction, fainted state changed, victory result emitted, world flag updated. Pokemon Showdown’s assert helpers in pokemon-showdown/test/assert.js chunk_f2ab14d37f397660 are useful here because they remind us that an assertion vocabulary can talk in domain language: stat stage changed, HP changed, full HP, fainted. It is not merely raw object equality.

Let’s pause and ask another check question. Suppose your test says, “after the player uses a Potion, save.party[0].hp equals 24.” Is that a good assertion? Maybe. It depends on the setup. If the creature’s max HP is 24 and the Potion amount is enough to heal to full, yes. But the more architectural assertion is two-part: the item operation changed party HP according to item rules, and the bag count decreased by one. If you only assert HP, a broken inventory system could pass. If you only assert item count, a broken healing system could pass. A regression test for the gameplay sequence has to catch both sides of the operation.

Now let’s talk about what the minimal clone versions actually contain. The map loader clone needs a map registry, not a whole map engine. A registry can have map ids, warp destinations, trigger coordinates, and maybe object events. That is enough to preserve the FireRed idea that map loading comes from data and sets up runtime state. We are not reimplementing field rendering. We are preserving the data-to-runtime boundary that has been part of this course since week one.

The script runner clone needs a tiny command language. Not a full scripting VM, but enough commands to demonstrate progression. Something like message, startBattle, setFlag, giveItem, branchIfFlag, end. In the test, the trigger coordinate selects a script id, the script emits startBattle, waits for a battle result, then resumes and sets a flag. That waiting behavior matters. It prevents the script runner from cheating by assuming victory immediately. It also mirrors the real architectural pattern where field logic, script execution, and battle callbacks interleave across time rather than running as one ordinary function call.

The party data clone needs creatures with species id, level, current HP, max HP, moves, maybe status, maybe experience. Do not add ability, nature, EVs, IVs, held item, friendship, and growth curves unless the test needs them. Remember our objective today: minimal clone versions. Minimal means it carries the invariant needed by the trace. It does not mean “empty.” If a battle assertion needs species and fainted state, then species and HP must exist. If item use needs max HP, then max HP must exist. If save persistence needs stable serialization, then the party model must round-trip cleanly.

The battle turn clone needs a seeded random source, a battle state, a choice interface, and a result log. Again, the comparison to Showdown is deliberate. Showdown tests are useful to us because createBattle and deterministic seed setup, as discussed in pokemon-showdown/test/TESTS.md chunk_134ac9f75b9d6477 and pokemon-showdown/test/common.js chunk_979fde4c3177ea4f, make the battle reproducible. And makeChoices, in pokemon-showdown/test/TESTS.md chunk_e8da8b9a442469fd, keeps the test at the level of player or side decisions. For our clone, we can say player chooses move 0, enemy chooses move 0, seed controls hit or damage variation, resolveTurn emits log entries and final state. The test should not reach into the random number generator halfway through the turn unless the purpose of the test is specifically random sequencing.

The item use clone needs item definitions and an operation that validates context. A Potion-like item can be used on a party member only if the count is positive and the target is not already at full HP, depending on the rules you choose. The operation returns an item result: healed amount, consumed true, maybe error code. This is a good place to notice how FireRed-style architecture resists a common beginner simplification. The bag is not just a number, and the party is not just a number. Item use crosses inventory and party data, but it should still happen through a deliberate operation. That is how we keep the test honest.

The save persistence clone needs serialization and continuation. The source packet points us to pokefirered/src/save.c chunk_9900b8674c89af66 for save continuation. We are not going to imitate every real save block detail in a small harness, but we are going to preserve the concept that saving is not a printout. It is a continuation point. It records enough state for the next boot or next load to resume in the correct world condition. So your save assertion should include the event flag, the map id, the player position, the party HP, and the bag count. If you cannot load the saved output and continue the same trace, you have not tested persistence. You have tested formatting.

Now, there is a design tension here, and I want to put it plainly. End-to-end tests are valuable because they catch integration failures. They are also dangerous because they can become vague. A test that says “play the game and expect success” is almost worthless when it fails. The way we avoid that is by making the trace explicit. Source map from data to behavior. That phrase belongs to this week for a reason. We want to be able to say, this map trigger came from the map registry; this script command came from the script table; this battle state came from party and encounter setup; this item operation came from item data; this saved value came from the persistence model.

That is also how week 24 connects to the whole course plan. In the early weeks, we decomposed the game into runtime shell, overworld maps, event scripting, creature data, battle, inventory, UI, save, and link systems. In the middle weeks, we traced data records through globals, callbacks, tasks, scripts, structs, and concrete source functions. In weeks 22 and 23, you had to identify the files, structs, functions, constants, tables, and state variables that implement major systems. Now the final move is verification. We are not just admiring the architecture. We are using it to decide what a correct regression test must say.

Let me give you a small example of a bad final trace, because this is where grading usually becomes easy. Bad trace: create player, call winBattle, set flag, save. Why is that bad? It skips map loading. It skips script execution. It skips battle turn choice. It skips item operation. It directly mutates progression. It may be useful for one tiny unit test, but it does not satisfy the week 24 objective.

Better trace: load save snapshot, enter map through warp, step onto trigger, script emits battle setup, battle starts with seeded state, choices are submitted, turn resolves, battle result returns to script, script sets event flag and gives reward, overworld resumes, item is used through inventory operation, save store persists a new snapshot. That is the shape I expect from your lab. The clone can be tiny. The trace cannot be fake.

Now, I want you to notice the role of logging. Debug logs and replay output are not just developer comfort. They are part of test diagnosis. In a battle regression test, especially one with seeded randomness, a replay log lets you see whether the failure came from setup, choice submission, random outcome, damage application, faint handling, or result propagation. Showdown’s testing culture is again useful as a comparison point because its tests commonly drive battles through creation and choices and then inspect domain outcomes. We can borrow that discipline without replacing FireRed as our source authority.

For the final architecture trace, I would want a log that reads almost like a compact script of the run. Loaded save slot one. Entered map ForestA from warp SpawnNorth. Player moved from ten five to ten six. Triggered script BugCatcher3. Script command startBattle trainer BugCatcher3. Battle seed 1234. Player active Charmander-like species. Enemy active Caterpie-like species. Turn one choices move zero and move zero. Player HP changed from twenty-two to eighteen. Enemy HP changed from twelve to five. Turn two choices move zero and move zero. Enemy fainted. Battle result victory. Script set flag BugCatcher3Defeated. Field resumed on ForestA at ten six. Used Potion on party index zero. HP changed from eighteen to twenty-four. Bag Potion count changed from one to zero. Persisted save slot one.

That log is not flashy. It is excellent because it is replayable by a human. If the final assertion fails and says Potion count expected zero but got one, you know where to look. If the flag is not set, you know whether the battle result returned to the script. If the map id is wrong after save, you know the return-to-overworld path is suspect.

Check for understanding again. Why should the save assertion happen after item use, not immediately after battle victory? Because the requested gameplay sequence includes both battle progression and post-battle item mutation before persistence. If you save immediately after victory, you have tested a different story. In architecture, sequencing is not cosmetic. The order of operations is part of the behavior.

Now, let me connect this to the exact source emphasis one more time. FireRed’s pokefirered/src/overworld.c chunk_7ea6afd1253d6817 is our anchor for map loading through LoadMapFromWarp. FireRed’s pokefirered/src/battle_setup.c chunk_494d132a59422c85 is our anchor for battle setup as a cross-system transition point. FireRed’s pokefirered/src/save.c chunk_9900b8674c89af66 is our anchor for persistence and continuation. Showdown’s pokemon-showdown/test/TESTS.md chunk_134ac9f75b9d6477, pokemon-showdown/test/common.js chunk_979fde4c3177ea4f, and pokemon-showdown/test/TESTS.md chunk_e8da8b9a442469fd are our comparison anchors for deterministic battle construction, seeded or forced randomness, and choice-driven tests. Showdown’s pokemon-showdown/test/assert.js chunk_26026d44764fa919 and chunk_f2ab14d37f397660 are our comparison anchors for a readable assertion vocabulary.

And that last phrase, readable assertion vocabulary, is the one I want you to carry into the lab. Do not make your final test read like memory offsets. Make it read like gameplay state. assertSpecies, assertFainted, assertHpChanged, assertFlagSet, assertItemCount, assertMapPosition, assertSavedSnapshot. The exact names are yours, but the intention should be clear. A professor, a teammate, or your future self should be able to read the test and understand the game story and the architecture story at the same time.

We are now in the part of the course where elegance means traceability. Not abstraction for its own sake. Not perfect simulation. Traceability. Can you point from the test sentence to the clone boundary, from the clone boundary to the FireRed architectural source, and from the source idea back to the player-visible result? If yes, your regression test is doing real architectural work. If no, it may still run, but it is not yet the week 24 standard.

So in the next segment, we will take that six-box harness and sharpen the assertions. We will ask which checks belong inside the battle test, which checks belong in the end-to-end trace, and which checks should remain deliberately absent because they would make the test brittle instead of informative.

Good. Let’s sharpen the assertions now.

When I say sharpen, I do not mean “add more assertions until the test looks impressive.” That is the beginner version of rigor. The advanced version is knowing which fact belongs at which layer. A battle regression test should be narrow enough that when it fails, you know what contract was broken. An end-to-end architecture trace should be broad enough that when it passes, you know the major state handoffs still work. Those are different virtues.

So, board cue here. I’m drawing two columns. Left column: “combat-local truth.” Right column: “world-persistent truth.” Under combat-local truth, I’m writing HP, fainted status, stat stages, active species, selected move outcome, item-use effect during battle. Under world-persistent truth, I’m writing map id, player position, script flags, party mutation after battle, bag count after item use, save block continuity.

Now ask yourself: if I use a Potion during a seeded battle, where should the assertion live? The HP increase itself belongs in the battle test. The inventory decrement may belong in a higher-level test if the item came from the real bag model, but in a minimal clone battle harness, it may be enough to assert that the battle item adapter was called with the expected item id and target. And the save persistence of the new bag count definitely does not belong inside the turn-resolution unit test unless the purpose of the test is specifically to trace from item use to saved state.

This is where the Showdown testing material is useful as a comparison point, not as the architecture we are copying wholesale. In pokemon-showdown/test/assert.js, chunk_26026d44764fa919, the assertion vocabulary includes things like species, fainted, and fullHP. In chunk_f2ab14d37f397660, it includes stat stage and HP-change assertions. That vocabulary is deliberately close to battle state. It does not say, “assert that the player is standing outside Pewter Museum after the save file reloads.” That would be the wrong assertion vocabulary for that layer.

Let me give a concrete example. Suppose our clone test says: create a deterministic battle, put a level 10 Charmander against a level 10 Bulbasaur, force the first move to hit, have Charmander use Ember, and assert that Bulbasaur’s HP decreased and Bulbasaur did not faint. That is a combat-local regression test. It is not trying to prove the whole RPG works. It is executable documentation for a combat rule: this move from this attacker against this defender produces a bounded state change. If we want to make it closer to Showdown style, we seed or force the randomness explicitly. The testing guide material in pokemon-showdown/test/TESTS.md, chunk_134ac9f75b9d6477, is important here because it highlights createBattle, seeds, and forceRandomChance as mechanisms for turning probabilistic mechanics into stable tests. And common.js, chunk_979fde4c3177ea4f, is relevant because createBattle options include deterministic seed support. The principle is not “use their exact helper.” The principle is “randomness is an input to the test, not an invisible atmosphere around the test.”

Check for understanding: if Ember has a burn chance, should our regression test usually assert “Bulbasaur is burned” after one Ember? No, not unless the test has forced that chance or seeded the battle in a way that makes that event intentional. Otherwise the test is not describing Ember; it is gambling on Ember. A good test can say, “when burn is forced, the target receives burn,” or “when burn is not forced, the damage still applies and the assertion does not depend on the secondary effect.” That distinction is the whole reason week 24 belongs after our simulator comparison lectures.

Now, connect that back to FireRed. FireRed is not organized as a neat test-first simulator library. It is a GBA game with globals, callbacks, tasks, scripts, and save blocks. We spent weeks learning not to complain about that shape, but to trace it. When the player moves from one map to another, map loading is not a pure function returning a value. The source focus for today points us to pokefirered/src/overworld.c, chunk_7ea6afd1253d6817, around LoadMapFromWarp. That is architectural evidence for the map-loading boundary. The player-visible result is simple: you appear on the target map at the target warp destination. The runtime story is broader: warp data flows into map state, map header/layout state becomes active, object events and scripts become relevant, and the overworld callback resumes with a different context.

So if I design the clone version, I do not need to reproduce every field in FireRed’s map header. I need a minimal LoadMapFromWarp-equivalent boundary. Maybe the clone has a MapRegistry, a currentMapId, a player position, and an active script table. That is enough to test the handoff from save or warp into playable map state. But I should name the boundary honestly. In your final architecture trace lab, if your box says “load map,” I expect you to connect it to LoadMapFromWarp in pokefirered/src/overworld.c, chunk_7ea6afd1253d6817, not because your clone is FireRed, but because your clone is modeling that architectural responsibility.

Now I’m going to add a third column on the board: “test double allowed?” This is the column that prevents overbuilding. For map loading, test doubles are allowed for tiles, rendering, and audio. They are not allowed for current map identity and player spawn position if the trace claims to test map entry. For script execution, test doubles are allowed for text boxes and sound effects. They are not allowed for event flags and battle-start commands if the trace claims to test a trainer encounter. For party data, test doubles are allowed for sprite assets and nickname rendering. They are not allowed for HP, species, level, moves, fainted status, and item mutation if the trace claims to test battle consequences.

Notice what we are doing. We are not ranking systems by importance. We are deciding which details are semantically load-bearing for the regression. That is a professional testing skill.

Let’s walk one complete mini-scenario. The saved game says the player is in Route 3, standing at coordinate 12, 8, with a party containing one level 9 Squirtle, two Potions in the bag, and a story flag saying a nearby trainer has not been defeated. The clone boots from the save. It calls the clone save continuation function, which restores player location, party data, inventory, and flags. In FireRed terms, when we discuss save continuation, the packet points us to pokefirered/src/save.c, chunk_9900b8674c89af66. Again, the clone does not need flash memory sectors. It needs the responsibility: serialized game state becomes live runtime state.

Then the trace enters map loading. The clone resolves Route 3 through the map registry and places the player at 12, 8. The player moves one tile. The movement checks the object event grid or trigger table and finds a trainer script. The script executor runs a command that starts a battle. In FireRed, battle setup is cross-module by nature; pokefirered/src/battle_setup.c, chunk_494d132a59422c85, is useful evidence because its includes and responsibilities sit at the boundary among overworld, script, item, event data, Pokémon state, and battle transition. That file is not just “battle logic.” It is glue between the field state and the battle state. That is exactly what your clone should preserve architecturally.

Then the battle begins. The battle harness receives party snapshots or references, depending on your design. Here is where we reuse prior knowledge without re-teaching it: from our Showdown turn-loop lectures, you already know the value of separating request state, player choices, and action queue resolution. In the clone, the trainer AI may just choose Tackle. The player chooses Water Gun. The deterministic seed or forced random adapter removes chance from the test. The turn resolves. Assertions check combat-local truth: active species is Squirtle, enemy HP changed, maybe the enemy fainted, maybe the player’s HP did not change if the opponent fainted before acting, depending on your speed and action model.

Then we return to world-persistent truth. If the trainer was defeated, the script sets the defeated flag. If the player used a Potion, the inventory count is now one. If Squirtle took damage, the party’s HP is now lower. Then the clone returns to overworld and persists state. The save assertion is not “the save bytes equal this enormous fixture.” That is brittle and not very educational. The assertion is “after saving and reloading, the defeated trainer flag remains set, the bag count remains one, and Squirtle’s HP remains the post-battle value.” That is architecture-level regression.

Pause there. What would be a bad assertion in that end-to-end test? One bad assertion would be “the debug log has exactly 137 lines.” Another would be “the internal script program counter equals 22 after the battle,” unless your lesson is specifically about script VM continuation. Another would be “the random seed has this final internal value” unless you are testing RNG consumption. Those are not impossible assertions, but they are expensive assertions. They tie the test to internal incidental sequencing rather than player-visible contract.

This is why the Showdown TESTS.md material around makeChoices matters. The packet points to pokemon-showdown/test/TESTS.md, chunk_e8da8b9a442469fd, for makeChoices and RNG-independent assertions. MakeChoices is valuable as a testing idea because it speaks in player actions: this side chooses this move, that side chooses that move. The assertions should then inspect resulting battle state. When possible, assert outcomes that remain true even if harmless internal ordering changes. For example, “the target is fainted” is usually a better regression assertion than “the log’s fourth line is exactly the damage line,” unless the log format itself is the API under test.

Now, I want to be careful with the word “minimal.” Minimal does not mean vague. Minimal means every included piece has a reason. A minimal party model for week 24 cannot just be an array of names. It needs enough creature data to support the trace. Species, level, current HP, max HP, moves, status if your scenario uses status, maybe held item if your scenario uses held item. If the scenario is only a Potion and a single attack, you do not need nature, EVs, IVs, friendship, ribbons, Pokédex ownership, mail, contest stats, or met location. Those matter elsewhere. They are not load-bearing here.

Likewise, a minimal script executor does not need the entire FireRed script command language. It needs a way to represent the commands in your sequence: check flag, lock player if you model that, start trainer battle, set defeated flag, give reward or call item update if relevant, return control to overworld. If your script executor is just a hard-coded if statement hidden in the map object, then you have erased the architecture. If it is a tiny bytecode or command-list interpreter, even with five commands, you have preserved the shape we care about: data-driven event behavior executed by a runtime interpreter.

Here is another check for understanding. Suppose your final trace says, “move player, trigger script, start battle.” Can you replace the script executor with a direct call to startBattle in the movement function? For a toy game, yes. For this course’s week 24 architecture trace, no, because the point is to preserve the architectural seam between overworld movement and event scripting. FireRed’s design repeatedly routes player-visible events through script and event-data machinery. We have seen that pattern all term. Your clone should be small, but it should not flatten the very boundary you claim to understand.

Let’s make that concrete on the board. I’m writing a tiny pseudo-flow, spoken rather than coded: SaveStore.load returns SaveState. SaveContinuation.apply installs currentMap, party, bag, flags. MapLoader.loadFromWarp resolves map id and spawn. Overworld.step attempts movement. TriggerSystem finds event. ScriptRunner executes trainer script. BattleSetup builds BattleState from parties and trainer data. BattleEngine requests choices. Choices resolve through deterministic RNG. ResultAdapter writes party HP, rewards, flags, and item counts. SaveStore.save persists the updated state.

Now, in that flow, where do we assert? After SaveContinuation.apply, assert current map and party exist. After MapLoader.loadFromWarp, assert player position and active map id. After ScriptRunner reaches the battle command, assert a battle was requested with the expected trainer id, not yet that the trainer was defeated. After BattleEngine resolves the turn or battle, assert HP and fainted state using battle vocabulary. After ResultAdapter, assert flags and inventory. After SaveStore.save and reload, assert persistence.

This staged assertion style is how you turn a trace into a diagnostic instrument. If the final reload fails, you want to know whether the battle never wrote party HP back, the flag never set, the save layer skipped the flag bank, or the reload layer ignored it. A single giant final assertion gives you confidence only when it passes. Staged assertions teach you when it fails.

And this is also where debug logs and replay output fit. Logs are not a substitute for assertions. Logs are a witness. In Showdown-style battle tests, replayable choices and deterministic setup make a failure reproducible. In our clone, a debug log should tell the story in architectural verbs: loaded save slot, loaded map from warp, stepped player north, triggered event 3, started trainer battle, resolved turn 1, applied result, saved state. The replay should be compact enough that a developer can rerun the scenario. But the pass or fail should come from explicit assertions, not from a human reading a transcript and deciding it looks okay.

Let me stress one advanced point: deterministic testing is not the same thing as removing randomness from the design. A creature RPG without chance is not the same game. Accuracy, critical hits, secondary effects, encounter rolls, damage variance, AI tie breaks, all of these are part of the domain. The testing discipline is to make randomness inspectable and controllable. The Showdown chunks on createBattle, seeds, and forceRandomChance, especially pokemon-showdown/test/TESTS.md chunk_134ac9f75b9d6477 and pokemon-showdown/test/common.js chunk_979fde4c3177ea4f, are evidence of that discipline. For your FireRed-inspired clone, the equivalent may be a RandomSource interface, a fixed seed, or a scripted random queue. The exact tool is less important than the contract: no test should fail because the universe shrugged.

Now, some of you are probably thinking, “But FireRed itself has hardware constraints and save flash behavior and callbacks, so isn’t our clone too clean?” Yes, it is cleaner. That is the point of a clone harness. But it must be clean in a way that reveals FireRed’s architecture, not clean in a way that replaces it with a generic RPG engine. If you remove callbacks, tasks, scripts, map loading, party mutation, and save continuation entirely, then your test may still describe a battle, but it no longer describes FireRed-style system integration.

So when we connect week 24 to the overall course plan, here is the synthesis so far. Weeks 1 and 2 taught you to see the runtime shell and generated data as architecture, not background noise. The middle of the course taught you to track maps, object events, scripts, party data, inventory, battle setup, UI, and save as cooperating systems. Weeks 22 and 23 asked you to identify concrete FireRed files, structs, functions, constants, data tables, and state variables. Week 24 raises the standard again: do not merely identify them. Build a small executable trace that proves you understand how the responsibilities hand off.

That is why your final lab sequence is deliberately broad: load save, enter map, move player, trigger script, start battle, resolve a turn, award result, update flags/items/party, return to overworld, and persist state. It is broad because the course is broad. It is small because regression tests must be maintainable. It is deterministic because architecture cannot be graded through chance.

Before we move to the next segment, quick oral check. I’ll say a claim, and you decide whether it belongs in a battle-local test, an end-to-end trace, or no test at this level. “After Water Gun, the opponent’s HP is lower.” Battle-local test. “After defeating the trainer and saving, the trainer does not challenge again after reload.” End-to-end trace. “The internal task id allocated during the transition is exactly 7.” Usually no test at this level, unless the assignment is specifically about task allocation. “The active species is Squirtle after battle setup.” Battle-local or setup-level assertion, depending on where you place the boundary. “The map loaded through the warp has the expected id and spawn coordinate.” End-to-end trace, or map-loader unit test if isolated.

Good. In the next part, we are going to take this same staged trace and talk about source maps from data to behavior. Not source maps in the JavaScript debugging sense, but an architectural source map: for every runtime event in the trace, what source file, chunk, table, function, or test helper gives us the authority to say, “this is the FireRed-shaped boundary”? That will let us turn the final lab from a diagram into an executable specification with evidence behind it.

Now, pick up that phrase, “architectural source map,” and let’s make it concrete. If I write on the board, “load save, enter map, trigger script, start battle, resolve turn, update inventory, persist,” that is not yet an architecture trace. That is a plot summary. A player could tell us that much. An architecture trace has to attach authority to each transition. It has to say, “this state came from this save structure or clone equivalent; this map transition is shaped like FireRed’s warp path; this battle setup is testable using the same deterministic strategy we see in Showdown’s test helpers; this assertion is not just an English expectation, it is a reusable vocabulary over combat state.”

So, let’s draw a vertical line down the board. On the left side, I am going to write runtime event. On the right side, I am going to write source authority. First row: “continue saved game.” The source authority, in our FireRed-centered trace, is pokefirered/src/save.c, chunk_9900b8674c89af66, because that is the week’s source focus for save continuation. In the clone, we do not need flash-sector complexity. We need the semantic boundary. Does loading a save restore the player’s map identity, coordinates, party, inventory, flags, and enough script state to continue? If yes, our clone has preserved the architectural obligation.

Second row: “enter map from warp.” For that we point to pokefirered/src/overworld.c, chunk_7ea6afd1253d6817, the LoadMapFromWarp focus. Again, do not clone the whole engine. Clone the boundary. A warp is not merely a coordinate assignment. It is a map loading decision that ties together map group, map number, warp id or destination metadata, player placement, object/event availability, and the callback path that returns us to overworld behavior. In a minimal clone, that might be a function called loadMapFromWarp(save, warp), returning a MapRuntime with layout id, object events, script triggers, and spawn coordinate. The name matters less than the contract.

Third row: “script starts battle.” Here the source map crosses modules. We have event script state, event flags, party data, and battle setup. The research packet gives us pokefirered/src/battle_setup.c, chunk_494d132a59422c85, and the excerpt itself is useful because look at the include surface: task, overworld, item, sound, pokemon, load_save, script, event_data, event_object_movement, field systems, battle, battle_transition. That include list is not trivia. It tells us battle setup is an architectural junction. It is where the pleasant fiction of “a battle starts” becomes a concrete handoff among overworld state, script state, player party state, and battle engine state.

Now, pause there. Check for understanding. If your clone has a startBattle function that takes only a player level and enemy species, what is missing? Yes, it may still run damage math, but it cannot test the FireRed-shaped boundary. It cannot prove that the battle was caused by a script on the loaded map. It cannot prove that flags or trainer defeat state are updated afterward. It cannot prove that returning to the overworld resumes the correct callback or map state. So it is a battle calculator, not an architecture regression test.

Let’s take a concrete example. Suppose our final lab sequence is: load a save in Viridian Forest, walk one tile north, trigger a trainer script, start a battle against a Bug Catcher’s Weedle, use Tackle on turn one, receive damage from Poison Sting, use a Potion on turn two, win the battle, set the trainer defeated flag, consume the Potion, return to the overworld, and save. I am deliberately making this plain. I do not want exotic mechanics in the first architecture trace. The sophistication is not in choosing a complicated battle; the sophistication is in preserving the chain of evidence.

On the board, I’ll sketch the minimal clone types. SaveState has map id, x, y, facing, party, bag, flags, and maybe a random seed. MapRuntime has map id, collision grid, object events, coordinate triggers, and scripts. ScriptVM has program counter, local variables, and commands like lock, message, trainerBattle, setFlag, giveItem, release. Party has Pokemon entries with species, level, hp, max hp, moves, status, experience. BattleState has sides, active battlers, turn number, request state, queue, log, and RNG. Inventory has item counts. Persistence has serialize and deserialize.

Notice what I did not include. I did not include full rendering, audio, wireless link, Pokédex, daycare, or summary screen. Those are real systems, and some of them have source packet representation, for example pokefirered/src/pokemon_summary_screen.c, chunk_e8e17a42bbf17d46, and pokefirered/src/daycare.c, chunk_119931105f546346. But they are not on the critical path for this trace. Advanced design is not “include everything.” Advanced design is knowing which boundaries must be represented for the behavior under test.

Now let’s connect Showdown. We are not replacing FireRed with Showdown. We are borrowing testing discipline. In pokemon-showdown/test/common.js, chunk_979fde4c3177ea4f, the focus is createBattle options and deterministic seed. That gives us a powerful pattern: a battle test should not depend on ambient randomness. When you instantiate a battle, you pass in enough options to define the format, teams, and seed. For our clone, createBattleForTrace should receive the two parties, battle type, starting seed, and perhaps a scripted random stream. If Poison Sting has a poison chance and our test is about Potion consumption, do we want poison to randomly occur? No. Either force it, if poison is the behavior under test, or prevent it, if it is noise. That same principle appears in pokemon-showdown/test/TESTS.md, chunk_134ac9f75b9d6477, with createBattle, seeds, and forceRandomChance as the testing pattern.

Here is the subtle point. Determinism is not the same as making the game non-random. Determinism means randomness becomes input. We can still test critical hits, accuracy misses, secondary effects, encounter rolls, and AI choices. We simply make the roll stream visible and replayable. This is why the debug log matters. Every time the clone asks for randomness, it should write something like, “rng accuracy Tackle: 42 under 95, hit,” or “rng secondary Poison Sting: 87 over 30, no poison.” In production you may not show that to players. In regression tests, it is gold.

Now, think back to our prior Showdown turn loop discussion. We already covered battle request state, player choices, commit flow, and action queue resolution, so I will not re-teach those. I want you to reuse them. In the trace, the battle should not mutate directly from “turn begins” to “hp changed” with no intervening choice record. You want a request: player may choose move, item, switch, or run depending on context. You want a committed choice: “move 1” or “item Potion target active.” You want action resolution. Then you assert afterward. That is exactly the discipline behind pokemon-showdown/test/TESTS.md, chunk_e8da8b9a442469fd, where makeChoices and RNG-independent assertions are the focus. The lesson is not the exact API spelling. The lesson is that choices and assertions form a readable executable specification.

Let me model a test out loud, not in code, just in architecture terms. Arrange: load SaveState S with player at map ViridianForestA, coordinate 10, 14, party containing level 8 Squirtle with known HP, bag containing one Potion, flag TRAINER_BUG_CATCHER_1_DEFEATED false, seed 1234. Act one: loadMapFromWarp or continueMapFromSave produces MapRuntime M. Assert: active map id is ViridianForestA, player coordinate is 10, 14, object event for Bug Catcher 1 exists. Act two: move player north. Assert: script VM begins trainer script, player input is locked, battle setup request is emitted. Act three: create deterministic battle from that request. Assert: player active species is Squirtle, opponent active species is Weedle, both full HP if the scenario says so. That assertion vocabulary is exactly what Showdown’s test assertions encourage us to name; pokemon-showdown/test/assert.js, chunk_26026d44764fa919, is the source focus for species, fainted, and fullHP assertions.

Now continue. Act four: make choice, Squirtle uses Tackle; opponent uses Poison Sting; resolve queue. Assert: HP changed by expected amount, nobody fainted, turn number advanced, battle log contains the move events in order. That pulls in the other Showdown assertion focus, pokemon-showdown/test/assert.js, chunk_f2ab14d37f397660, for statStage and HP-change assertions. Even if FireRed’s exact damage formula differs from our minimal clone, the test vocabulary is still correct: assert HP changed, assert stat stage changed, assert fainted state, assert species, assert full HP when appropriate. Do not assert pixels. Do not assert task slot numbers unless task allocation is the behavior being tested. Assert the contract at the level where the behavior belongs.

Then act five: choose item Potion, target Squirtle. Here our minimal clone needs inventory and battle to cooperate. Item use is not just “hp equals hp plus twenty.” The bag count decreases. The battle request is consumed. The item action occupies the turn in the action queue. The healing event is logged. If the target is already full HP, the action should fail or be rejected according to the game rules your clone is modeling. In FireRed architecture terms, item use sits across item data, party data, battle context, and UI request state. The packet’s battle_setup.c excerpt, chunk_494d132a59422c85, includes item and pokemon among the dependencies around battle setup, which is a useful reminder that these systems are not isolated islands.

Now, another check for understanding. Suppose your test says, “after using Potion, Squirtle has more HP.” Is that enough? No, not for the final lab. It is a partial battle assertion. A stronger regression assertion says: Squirtle’s HP increased by the item’s healing amount capped at max HP, bag Potion count decreased from one to zero, the battle log records item use before opponent action if your priority model says so, and the save state after persistence contains the zero Potion count. The final clause matters because the lab is not “does healing work?” The lab is “can we trace from loaded save through gameplay and back to persisted state?”

Now I want to make the source-map idea precise. For every assertion in your final trace, ask, “what source family authorizes this?” If you assert map identity after warp, you cite pokefirered/src/overworld.c, chunk_7ea6afd1253d6817, because LoadMapFromWarp is the boundary. If you assert save continuation restores enough state to resume, you cite pokefirered/src/save.c, chunk_9900b8674c89af66. If you assert deterministic battle construction, you cite pokemon-showdown/test/common.js, chunk_979fde4c3177ea4f, and pokemon-showdown/test/TESTS.md, chunk_134ac9f75b9d6477. If you assert choice-driven turn resolution, you cite pokemon-showdown/test/TESTS.md, chunk_e8da8b9a442469fd. If you assert species, fainted, full HP, HP delta, or stat stage, you cite pokemon-showdown/test/assert.js, chunk_26026d44764fa919, and chunk_f2ab14d37f397660. If you assert battle setup as a junction among overworld, scripts, item, pokemon, and battle modules, you cite pokefirered/src/battle_setup.c, chunk_494d132a59422c85. That is how your diagram becomes evidence-backed.

Let’s talk about debug logs and replay output, because this is where many final projects become hard to grade. A diagram can look correct while hiding nondeterminism. A replay log exposes the chain. I want the clone to produce a compact trace, something like: loaded save slot; restored map ViridianForestA; loaded map via continuation; player moved from 10,14 to 10,13; trigger trainer sight script; battle request created; battle seed 1234; turn one request move; player chose Tackle; enemy chose Poison Sting; damage events; turn two request; player chose Potion; bag count changed; battle ended; flag set; overworld resumed; save serialized. That replay is not merely debugging convenience. It is the final architecture trace in executable form.

And yes, there is a cost. Logging can become noise. So we need a logging vocabulary that matches the systems in the course: save, map, script, party, battle, item, flag, persistence. Do not log every local variable. Log boundary transitions and state commitments. If a script command checks a flag, log the flag name and result. If a map loads, log the map id and source, warp or continuation. If battle starts, log the battle request id or trainer id. If a turn resolves, log choices and resulting HP deltas. If persistence happens, log the state version and key fields. Then your regression failure can say, “expected trainer defeated flag after battle, but replay shows battle ended without script resume.” That is an architectural bug, not just a failed assert.

Now, let’s connect this to the overall course plan. Week one gave us the runtime shell: startup, main callbacks, task dispatch, and the idea that behavior emerges through scheduled state machines. Week two and after gave us data records and generated or table-driven content. The overworld weeks gave us map headers, layouts, object events, warps, and script triggers. The data weeks gave us species, moves, items, and party records. The battle weeks gave us requests, choices, queues, and resolution. The save weeks gave us persistence and continuation. Weeks twenty-two through twenty-four pulled this together into traceability: not “I know where battle code is,” but “I can follow one player-visible sequence through data, globals, callbacks, scripts, structs, and tests.”

That is why today’s title is “Battle Mechanics as Regression Tests,” not “Battle Mechanics Unit Tests.” A unit test can prove a damage formula. Useful, but insufficient. A regression test protects a behavior that once worked across system boundaries. In a creature RPG, battles are perfect regression anchors because they touch almost everything. A battle begins from map or menu context. It consumes party state and item state. It uses deterministic or controlled random inputs. It produces logs, HP changes, fainting, experience, flags, money, and overworld continuation. If your battle regression test is well designed, it becomes a miniature whole-game trace.

Let me give one more example at a higher level of sophistication. Imagine a trainer battle that is supposed to be single-use. The first time, the trainer sees the player, starts battle, loses, sets a defeated flag, and changes post-battle dialogue. The second time, after saving and reloading, the trainer should not start battle again. A weak test checks only that the first battle can be won. A strong architecture regression test runs the sequence twice across persistence. First run: flag false, battle starts, flag becomes true, save writes true. Second run: load save, map loads, trainer object still exists, script checks flag true, dialogue branch runs, no battle request is created. That one test covers script execution, battle setup, flag mutation, save persistence, and map re-entry. It is small, but it is system-shaped.

This is also where we must be honest about minimal clone design. Your clone is not FireRed. It does not need to reproduce every table, every text box, every memory address, or every callback slot. But it must preserve the shape of causality. The saved game causes a map context. The map context causes available scripts. The script causes battle setup. The battle setup causes a deterministic battle state. Choices cause queued actions. Actions cause party, item, and flag changes. Those changes cause different future behavior after saving and loading. If you preserve that chain, you have learned the architecture.

Final check before we synthesize. I’m going to say three assertions, and you classify them mentally. “After turn one, Squirtle’s HP decreased by 4.” That is a battle-state assertion. “After the trainer loses, TRAINER_BUG_CATCHER_1_DEFEATED is true.” That is a script/progression assertion after battle result handling. “After reload, stepping into the trainer’s sight line does not create a battle request.” That is an end-to-end regression assertion crossing save, map, script, and battle setup. Good. The third one is the kind of assertion that tells me you understand the course.

So here is the closing synthesis for today. We began with battle tests because Showdown gives us a clean testing language: create a battle with controlled options and seed, make choices explicitly, and assert combat state with named helpers. We then pulled that discipline back into FireRed’s architecture, where a battle is not an isolated simulator event but a junction among save continuation, map loading, scripts, party data, item use, battle setup, overworld return, and persistence. The source map is our guardrail: pokefirered/src/overworld.c, chunk_7ea6afd1253d6817, for the warp-shaped map boundary; pokefirered/src/save.c, chunk_9900b8674c89af66, for continuation and persistence; pokefirered/src/battle_setup.c, chunk_494d132a59422c85, for the cross-system battle handoff; and the Showdown test chunks for deterministic setup, choice commitment, and assertion vocabulary.

For the workshop, your deliverable is not a large engine. It is a small engine whose trace is hard to misunderstand. I want one gameplay sequence, one replay log, one set of assertions, and one source-backed explanation for why each boundary exists. If your clone can load a save, enter a map, move the player, trigger a script, start and resolve a deterministic battle turn, use an item, update flags and party state, return to the overworld, and persist the result, then you have built a miniature of the full course architecture. That is the point of week twenty-four: to stop treating systems as chapters and start treating them as one continuous machine.

## Source References
- pokefirered/src/battle_setup.c
- pokefirered/src/daycare.c
- pokemon-showdown/data/mods/gen9ssb/moves.ts
- pokefirered/src/pokemon_summary_screen.c
- pokemon-showdown/data/text/moves.ts
- pokefirered/src/learn_move.c
