# Move Eligibility and Learnset Tables

Lecture ID: lecture_w10_d02
Week 10, Day 2

## Objectives
- Decompose a complete creature-collector RPG into runtime shell, overworld maps, event scripting, creature data, battle, inventory, UI, save, and link systems.
- Connect week 10 material to the overall course plan.

## Transcript
All right, let’s pick up exactly where we left off last time. In the previous lecture, we treated a party creature as a runtime object whose battle stats are not simply stored numbers, but computed results. We looked at species base stats, level, individual values, effort values, nature-like adjustments where relevant to the generation, current HP handling, and the distinction between persistent creature identity and derived battle readiness. Today we keep that same mental model, but we move sideways from stats into moves. More specifically, we are going to ask a deceptively simple question: when is a creature allowed to know a move?

Now, I want you to hear the wording carefully. I did not ask, “What moves does this creature currently have?” That is a smaller question. A current party member has move slots. Those move slots have move IDs and PP values, and we already treated that as part of the creature record and runtime mutation path. The larger architectural question is eligibility. Could this species learn this move by leveling? Could it learn this TM? Could it inherit the move through a breeding system? Could it relearn a move that was skipped? Could it retain a move across evolution? Could an item-use menu show this creature as compatible without mutating anything yet? Those are not all the same operation.

So let me put a phrase on the board: eligibility is not possession. Possession is the four-slot state currently stored on the creature. Eligibility is a query over species data, move tables, progression rules, and sometimes history. In a data-driven creature RPG, this distinction matters enormously, because you want many systems to ask eligibility questions without duplicating move-learning rules. The battle engine cares about possession: what moves can be selected right now, how much PP remains, what disabled or encored or sealed move constraints are active. The inventory system cares about eligibility: when the player selects a TM or HM item, which party members can receive it? The level-up flow cares about eligibility plus timing: did this creature reach a level at which one or more moves become available? The move relearner cares about eligibility across a species’s earlier level-up move table. Evolution cares about transition: do we recalculate, preserve, add, or offer anything?

This is why week 10 sits where it does in the course. In weeks 9 through 11, we are no longer merely identifying tables or callbacks. You have already been assessed on decomposing the runtime shell, overworld maps, scripting, creature data, battle, inventory, UI, save, and link systems. Homework and quiz work around week 8 asked you to identify files, structs, functions, constants, data tables, and state variables. Homework 9 pushed you to design minimal clone versions of map loading, script execution, party data, battle turns, item use, and persistence. So at this point I am going to assume you can follow a data table into runtime behavior. The sophistication now is in separating adjacent rules that look similar at the player level but belong in different architectural layers.

Let’s use FireRed as the primary authority. The research packet points us to several source areas today. For runtime calculations and mutations, we are focused on pokefirered/src/pokemon.c, especially chunk_4573760bdda53819 for Pokémon runtime calculations, chunk_f56e3a6588d98715 for data mutation and lookup, and chunk_c089f03b70501fda for creature state helpers. For species records, the packet directs us to pokefirered/src/data/pokemon/species_info.h, chunk_2e759c52d0f4129a. For move-learning UI behavior, it flags pokefirered/src/learn_move.c, chunk_af3598df6f092723, whose excerpt explicitly frames the move relearner as a state machine. And for the modern comparison point, it names pokemon-showdown/data/learnsets.ts, with the warning that this is high-signal for modeling learnsets, but not FireRed mechanics. That warning is important. Showdown is useful for seeing another architecture’s data modeling choices, not for retroactively defining how FireRed behaves.

Let’s start with the simplest player-visible case: a level-up move. Imagine a three-species test set for the lab. Species A learns Tackle at level 1, Vine Whip at level 7, and Razor Leaf at level 13. Species B learns Scratch at level 1 and Ember at level 7. Species C evolves from Species A at level 16 and has its own table, maybe Sleep Powder at level 15 and Body Slam at level 20. The naive implementation says, “When the creature levels up, scan the table and teach any move whose level equals the new level.” That is a good first draft. But immediately, advanced design pressure appears. What happens if the creature gains two levels at once? What happens if evolution occurs at the same level as a move? What if two moves are listed at the same level? What if the move slots are full? What if the player declines? What if the creature is generated above level 1? What if a later system asks not “what should happen now,” but “what moves are relearnable?”

Pause there. Check your understanding with me. If I ask, “Can this party member currently use Ember in battle?” what state do you inspect first? You inspect the creature’s current move slots and PP. If I ask, “Can Charmander learn Flamethrower from a TM?” what do you inspect first? You do not inspect its current four moves except maybe to prevent duplicate teaching. You inspect species compatibility data for that TM. If I ask, “Should this creature be offered a move after leveling from 6 to 7?” now you inspect level-up learnset data for the species and the level transition. Three questions, three different entry points.

On the board, I’m drawing three boxes. First box: current move slots. Second box: level-up learnset table. Third box: TM/HM compatibility bitset. Now I’m drawing arrows from different systems into these boxes. Battle points to current move slots. Level-up points to the level-up table, then mutates current move slots if the player accepts. Inventory points to the TM/HM bitset, then mutates current move slots if item use succeeds. Summary screen points mostly to current move slots, because it displays what the creature knows. Move relearner points back into learnset history. That is the architecture. It is not one giant function called CanKnowMove with every rule jammed into it. You can build such a function as a facade if you need it, but internally these rules need separate data sources.

This separation appears in FireRed’s organization. The core creature helper and mutation work lives around pokefirered/src/pokemon.c, including the chunks the packet calls out: chunk_4573760bdda53819 for runtime calculations, chunk_f56e3a6588d98715 for mutation and lookup, and chunk_c089f03b70501fda for state helpers. The move relearner’s screen and flow, on the other hand, are not simply buried in battle code. The packet excerpt for pokefirered/src/learn_move.c, chunk_af3598df6f092723, includes UI and task-related includes, and it describes a MoveRelearnerStateMachine with initialization through CB2_MoveRelearner_Init. That tells us something architectural: move eligibility is not merely a pure data question; the player-facing act of changing a move is mediated by a callback and menu state machine. But we should not confuse the UI state machine with the underlying learnset data.

Let’s make that concrete. Suppose Bulbasaur currently knows Tackle, Growl, Leech Seed, and Vine Whip. The player levels up and reaches the level where Poison Powder becomes available. The level-up resolver has a candidate move: Poison Powder. It then asks whether the creature already knows that move. If not, it asks whether there is an empty move slot. If yes, it can insert the move and initialize PP according to the move’s max PP. If no, it must launch the “forget a move?” flow. Architecturally, that flow crosses from data resolver into UI task state. It is not just a table lookup anymore. It is a transactional mutation with user choice.

This is a good place to connect back to the systems decomposition objective for the course. The runtime shell provides the callback and per-frame task machinery. We already covered AgbMain startup, SetMainCallback2, task allocation, task IDs, and RunTasks. The overworld can trigger encounters, item use, menu opening, and scripted rewards. The event scripting system can award TMs or cause battles or evolve a creature through special cases. The creature data system stores party and boxed creature state. The battle system consumes current moves. The inventory system initiates TM/HM use. The UI system renders compatibility, confirmation, and forget-move prompts. The save system persists the resulting creature record. Link systems must serialize or validate parties in contexts where move data matters. Today’s topic, then, is not isolated. Move eligibility is one of those places where the whole RPG architecture touches a compact piece of data design.

Now, let’s talk about level-up tables as a representation problem. A level-up learnset is usually a sequence of pairs: level and move. You can encode that as an array of small structs, as packed integers, as generated constants, or as external data compiled into source. FireRed’s decompilation style gives us C tables and constants rather than runtime JSON. That fits the toolchain story we covered back in week 2: generated data versus runtime-loaded data, binary embedding, template-driven source generation, and headers as cross-module contracts. Here, the contract is simple: given a species ID, code can find that species’s learnset information and iterate it in a known format.

But the semantics are less simple than the representation. Is the table ordered? Usually yes, and many routines rely on that. Does it contain level 1 moves? Yes, commonly. Does the code stop on a sentinel? Often, yes, because fixed-size arrays would waste space. Does a move relearner scan all moves below or equal to the current level? Very likely in concept, but the exact FireRed behavior must be taken from the source path responsible for that feature, such as pokefirered/src/learn_move.c, chunk_af3598df6f092723, in combination with the Pokémon data helpers in pokefirered/src/pokemon.c. The principle I want you to carry into your lab is that a level-up table is not “the moves this Pokémon has.” It is a timeline of opportunities.

I’ll say that again because it prevents a lot of clone-engine bugs: a level-up learnset is a timeline of opportunities. If your engine treats it as a set, you lose ordering, duplicate-level cases, and level transition semantics. If your engine treats it as current state, you accidentally overwrite the player’s choices. If your engine treats it as a menu list without filtering, you allow illegal relearns or future moves. The player’s four-slot set is the result of opportunities plus choices plus constraints.

Now let’s move to TM and HM compatibility. The packet names pokefirered/src/data/pokemon/tmhm_learnsets.h as the high-signal FireRed table for this lecture. A TM/HM learnset table is typically not shaped like a level-up table. It is not a timeline. It is a compatibility mask. For each species, the table says which machine moves that species can learn. In FireRed-style architecture, this is often implemented as a bitset, because you have a fixed universe of TM/HM items or machine move indices. Bit 0 means compatible with one machine, bit 1 means compatible with the next, and so on. The exact mapping from item to bit index must be consistent across item data, move constants, and compatibility table code.

On the board, I’m writing: species ID plus machine index gives boolean compatibility. That is the core TM/HM query. It does not care what level the creature is. It usually does not care which moves the creature might learn naturally. It may care about species form or special cases if the generation has them, but in the FireRed scope we should be conservative and source-grounded. The compatibility bitset answers a yes-or-no question quickly, which is perfect for inventory UI. When you highlight a TM in the bag, the game can mark party members as able or unable. When you select a compatible member, then and only then do you enter the move replacement path.

Here is the concrete example. Let’s say our lab has three species. We define four machine moves: TM01 Focus Punch, TM02 Dragon Claw, TM03 Water Pulse, and HM01 Cut. Species A’s bitset has TM03 and HM01 set. Species B’s bitset has TM01 set. Species C’s bitset has TM03 set but not HM01. If the player uses Water Pulse, Species A and Species C are eligible. If Species A currently has four moves, the UI must ask whether to forget one. If Species C has an empty slot, the mutation can be direct. If Species B already knows Water Pulse somehow through debugging or a gift script, the TM item flow still needs a duplicate check, because compatibility and possession are separate. A species can be compatible with a TM it already knows, but teaching it again should not consume the item or duplicate the move.

Check for understanding: if a species can learn Water Pulse by level-up, does that automatically mean its Water Pulse TM bit should be set? Not necessarily. That is a design decision encoded in the data. In Pokémon games, many moves overlap between natural learnsets and TM lists, but the architecture should not infer one from the other unless the source data explicitly chooses that policy. FireRed’s data-driven approach lets the designers set compatibility directly. Your clone engine in lab should do the same. Do not invent compatibility by scanning level-up moves.

This is where the modern comparison to Pokemon Showdown is useful, but only if we keep it in its lane. The research packet names pokemon-showdown/data/learnsets.ts as high-signal modern learnset modeling. Showdown’s learnset data has to support competitive legality across generations, transfer paths, event distributions, and modded rule sets. It is a battle simulator and rules database, not a GBA cartridge architecture. So we can compare the modeling impulse: both systems need to answer “how could this species know this move?” But we should not treat Showdown’s representation as FireRed’s mechanics. The packet explicitly says not to treat pokemon-showdown/data/learnsets.ts as FireRed mechanics. That is a professional source-use habit: useful comparison, wrong authority for cartridge behavior.

Let’s bring in the move relearner, because it reveals the difference between data lookup and player workflow. The excerpt from pokefirered/src/learn_move.c, chunk_af3598df6f092723, includes many UI and engine headers: global, gflib, script, task, data, field_fadetransition, overworld, menu, list_menu, event_data, text_window, pokemon_summary_screen, graphics, strings, moves constants. That include cluster tells a story. The move relearner is not just asking a pure question. It is entered from the field or a script context, fades the screen, builds a list menu, responds to directional input, displays text windows, may transition to the summary screen or move selection, and eventually mutates a creature’s known moves. The comment in that chunk identifies a MoveRelearnerStateMachine and names CB2_MoveRelearner_Init. So, once again, the source reinforces our system decomposition. Data answers what is possible; callbacks and tasks conduct the interaction; creature mutation functions commit the result.

If you are designing this in your own engine, you want three layers. First, a pure query layer: get level-up moves for species up to level, test machine compatibility, test already knows move, find empty slot. Second, a policy layer: at level-up, choose which candidate moves to offer and in what order; for relearner, filter moves by current species and current level; for TM use, check compatibility and item constraints. Third, an interaction layer: show menus, ask for confirmation, select move to forget, play sound, consume item, update PP, return to previous callback. FireRed’s C code is not arranged in exactly these modern abstraction words, but the system boundaries are visible in the source organization and include graph.

Let’s do a quick board trace. Player uses TM03 from bag. Inventory UI identifies the item as a machine. Machine index maps to a move ID, say Water Pulse. Party menu displays compatibility by querying species bitsets from the TM/HM learnset table. Player selects Species A. Code checks whether Species A is compatible. It checks whether Species A already knows the move. It checks for an empty slot. If no empty slot exists, it enters the forget-move UI. The player chooses Growl. Mutation writes Water Pulse into that slot and sets PP based on the move’s PP data. Inventory consumes the TM if the generation’s item rules say it is consumable, while HMs are usually not consumed. The save system later persists the party record. During battle, the battle system does not ask “was Water Pulse learned by TM?” It sees Water Pulse in a move slot with PP and applies battle move rules.

That last sentence is important. Once a move is in the slot, most battle behavior is source-agnostic. Battle does not usually care whether the move came from level-up, TM, egg, event, or relearner. Legality matters at acquisition time, trading validation, or external tools. Battle cares about move ID, PP, target, type, category, accuracy, flags, effects, and current battle conditions. The packet’s broader source list includes battle files like pokefirered/src/battle_setup.c, chunk_494d132a59422c85, and pokefirered/src/battle_util.c, chunk_9335e5727aa0657d. Those support the larger course idea that battle is its own runtime system. But today we are deliberately staying at the boundary: how moves arrive on the creature record before battle consumes them.

Now let’s connect evolution. Evolution is a species transition, not a total replacement of the creature record. We covered creature records and species info in recent lectures, so I will not re-teach BoxPokemon and Pokemon representation. Carry forward the key idea: a creature has persistent identity and stored fields, and species is one of the data keys that changes under evolution. When species changes, base stats and species info lookup change. That is why last lecture’s stat recalculation mattered. For moves, species transition raises another eligibility question. After evolution, which level-up table is authoritative for future level-ups? The evolved species’s table. What happens to current moves? They are generally retained unless a specific rule mutates them. What about a move that the pre-evolution could learn but the evolution cannot? Current possession can persist even if future eligibility differs. Again, eligibility is not possession.

Here is the example. Species A learns Vine Whip at level 7. Species C, its evolution, does not list Vine Whip in its level-up table. If Species A evolves into Species C at level 16 while knowing Vine Whip, your engine should not delete Vine Whip merely because Species C’s current table lacks it. That would confuse players and destroy legitimate history. But if Species C tries to relearn moves later, the relearner policy may use Species C’s level-up learnset, not Species A’s, depending on the game’s rules. So the species transition changes future eligibility queries without rewriting the whole past. This is subtle, and it is exactly the kind of bug that shows up when students model learnsets as “the set of moves this species may currently have” rather than as acquisition routes.

Let’s take a breath and recap the lecture so far. We have one stored four-slot move state. We have at least two major eligibility tables: level-up timelines and TM/HM bitsets. We have UI state machines that turn eligibility into player choices, with pokefirered/src/learn_move.c, chunk_af3598df6f092723, giving us a clear FireRed example of move-learning interaction being mediated through callback and menu machinery. We have core Pokémon data helpers and mutation paths in pokefirered/src/pokemon.c, especially the chunks named in the packet, chunk_4573760bdda53819, chunk_f56e3a6588d98715, and chunk_c089f03b70501fda. We have species data in pokefirered/src/data/pokemon/species_info.h, chunk_2e759c52d0f4129a, serving as part of the stable species-level input. And we have pokefirered/src/data/pokemon/tmhm_learnsets.h as the named compatibility table for FireRed TM/HM learning.

For the lab, your assignment will deliberately compress this into a three-species test set, because I want you to feel the architecture without hiding behind the full Pokédex. You will write a species table plus stat calculator, then add a TM/HM compatibility bitset and a level-up move resolver. The important grading criterion is not whether your toy data resembles the commercial data. The important criterion is whether your architecture preserves the separation of current move state, level-up opportunity, machine compatibility, and mutation workflow. If your stat calculator already takes species ID and level and returns derived stats, your move resolver should feel like its sibling: species ID and level transition go in, candidate move opportunities come out. Then a separate function should apply those candidates to the creature record under move-slot rules.

I’ll stop this first segment here, not because the topic is complete, but because the next step needs its own careful treatment. Next we will go deeper into bitset layout, sentinel-terminated learnset iteration, duplicate prevention, and the exact edge cases around multiple moves at the same level. Keep the phrase from the board in mind as we move forward: eligibility is not possession.

So, let’s pick up exactly there. Eligibility is not possession. I want you to hear that as an architectural boundary, not as a trivia sentence about Pokémon. Possession is what is stored on the creature record right now: move slot one, move slot two, move slot three, move slot four, and their PP values. Eligibility is a question asked against tables: could this species learn this move through this channel under these conditions?

On the board, I’m going to draw three boxes. The first box says current creature state. The second says species learnset data. The third says mutation workflow. If those boxes blur together in your implementation, the design becomes brittle. If they remain separate, then battle, party menu, move relearner, evolution, daycare, TM use, and scripts can all ask related questions without secretly corrupting one another.

Now, the reason this matters in FireRed is that the runtime creature is not the species table. We spent last time connecting species data to battle stats, and we used the `Pokemon` and `BoxPokemon` representation as a foundation rather than re-teaching it. Keep that foundation active. The creature record owns the individual’s current condition. The species info table owns default facts about the species. The move eligibility tables own pathways by which moves become available. Those are separate authorities.

In the FireRed source focus for this week, `pokefirered/src/pokemon.c` is important because its chunks are where runtime calculations, mutation, lookup helpers, and creature state helpers cluster together. The research packet names `pokefirered/src/pokemon.c` `chunk_4573760bdda53819` for Pokémon runtime calculations, `chunk_f56e3a6588d98715` for Pokémon data mutation and lookup, and `chunk_c089f03b70501fda` for creature state helpers. That grouping is exactly the architecture we are studying: not one giant “monster class,” but a set of small access and calculation pathways around a compact stored record.

Let’s make this concrete. Suppose we have a species called Emberling. Emberling has a level-up learnset that says level 1 Tackle, level 1 Growl, level 7 Ember, level 13 Quick Attack. Separately, Emberling’s TM/HM bitset says it can learn TM10 and TM35, but cannot learn HM03. And the creature instance, the actual Emberling in the player’s party, currently has Tackle, Growl, Tail Whip, and no fourth move. Its current level is 6.

Now Emberling gains a level and becomes level 7. What question do we ask? Not “what moves does Emberling have?” We already know that. Not “what moves can Emberling ever learn?” That is too broad. The level-up resolver asks, “between the old level and the new level, which level-up moves became newly available?” If old level is 6 and new level is 7, Ember appears as a candidate. That candidate then goes to a mutation workflow, and the mutation workflow asks the current creature record, “is there an empty move slot?” If yes, write Ember into that slot and initialize the PP from the move’s base PP. If no, launch the learn-move UI flow where the player chooses whether to forget an old move.

Pause there. Check your understanding. If Emberling can learn TM35, should gaining level 7 automatically teach it TM35? No. TM compatibility is eligibility through an item-use channel. Level-up learnset eligibility is a different channel. Both may point to the same move sometimes, but the route matters because the caller matters.

That is why the lab this week asks you to build a species table plus stat calculator, then add both a TM/HM compatibility bitset and a level-up move resolver for a three-species test set. I do not want one function called “learn all possible moves.” That function sounds convenient and it is architecturally wrong. I want a resolver for opportunities and a separate applier for creature-state mutation.

Now let’s go deeper into the table formats, because this is where many advanced students accidentally make a high-level model that is clean in Python or TypeScript but does not resemble a GBA-era data layout.

A level-up learnset in a C project like FireRed is usually compact and sequential. Conceptually, it is an array of encoded entries. Each entry combines a level and a move. The table continues until a sentinel value says, “stop reading.” The exact macro names are less important for our architecture than the pattern: iterate from the start, decode each pair, test the level condition, collect or apply candidate moves, and stop when the sentinel appears.

I’m writing this loop shape on the board in prose, not C syntax. Start at entry zero. While entry is not sentinel, decode level and move. If decoded level equals the level we are resolving, consider the move. Advance to the next entry. End when sentinel is reached.

There are two important consequences. First, the table itself does not need to store its length. The sentinel is the length boundary. Second, order is meaningful. If two moves appear at the same level, they appear in table order, and the runtime can process them in that order.

That second point is not cosmetic. Imagine Emberling reaches level 10 and the table has two level 10 moves: Flame Wheel and Focus Energy. If the creature has two open slots, both can be learned cleanly. If it has one open slot, the first move may fill the empty slot, and the second move may trigger a forget prompt. If it has no open slots, both may require separate decisions. If your resolver collapses candidates into a set, or sorts them alphabetically, or deduplicates them by level before preserving order, you have changed player-visible behavior.

Now, someone may ask, “Would commercial data really put multiple moves at the same level?” Yes, this is a normal enough case that your resolver should not assume one move per level. The broader point is that a learnset is not a map from level to move. It is more accurately a list of opportunities, each opportunity having a level and a move. In your lab implementation, I would much rather see `[(1, Tackle), (1, Growl), (7, Ember)]` than `{1: Tackle, 7: Ember}`, because the latter representation has already lost information.

This is also where we connect to the modern secondary comparison source carefully. The research packet names `pokemon-showdown/data/learnsets.ts` as a high-signal modern learnset modeling file, but it explicitly says not to treat it as FireRed mechanics. That distinction matters. Pokémon Showdown often models learnability across generations, transfer paths, event sources, breeding, tutors, machines, and legality rules in a rich data structure. That is useful as a comparison for how learnsets can become a legality model. But FireRed’s runtime architecture for level-up moves and TM/HM checks is much closer to compact tables feeding menu and mutation code. So if you borrow the idea that learnsets can be source-tagged, fine. If you import Showdown’s model wholesale into your FireRed-style engine, you are solving a different problem.

Let’s turn to TM/HM compatibility. In a creature RPG, a TM or HM item is not just an item that contains a move. It is an item that initiates a compatibility query. The item system says, “the player selected this machine.” The party menu says, “which party members are eligible?” The move data says, “this machine corresponds to this move.” The species compatibility table says, “this species can or cannot learn it.” Then, if the answer is yes, the move mutation workflow tries to add the move to the creature’s current move slots.

The FireRed packet names `pokefirered/src/data/pokemon/tmhm_learnsets.h` as the high-signal FireRed TM/HM learnset table. Even without reprinting the table, the architectural lesson is the bitset representation. A bitset is the correct shape when you have many yes-or-no eligibility answers over a fixed universe of machine IDs. Instead of storing a list like “Bulbasaur can learn TM06, TM09, TM10,” you store bits. Machine index 0 maps to one bit, machine index 1 maps to another bit, and so on. To test compatibility, compute the byte or word position and mask the bit.

On the board, I’ll write the mental formula. Index divided by bits per storage unit gives the storage element. Index modulo bits per storage unit gives the bit position. Then you AND with one shifted by that bit position. If the result is nonzero, eligible. If zero, not eligible.

For an advanced learner, I want you to notice two design advantages. The first is memory density. On hardware like the GBA, a fixed compatibility universe encoded as bits is far cheaper than arrays of integers per species. The second is call-site simplicity. The TM-use UI does not need to scan arbitrary lists or parse source tags. It can ask a near-constant-time question: species, machine index, yes or no.

But now the trap: a bitset answers only the compatibility question. It does not teach the move. It does not check whether the Pokémon already knows the move. It does not check whether there is an empty slot. It does not subtract the TM item if the game’s rules require consumption. It does not initialize PP. It just says yes or no for this species and this machine.

This same separation appears in UI flows. The research packet highlights `pokefirered/src/learn_move.c` `chunk_af3598df6f092723`, describing the move relearner state machine and its menu states. That is a good reminder that learning a move can be interactive. Once there are four moves, the operation is no longer a simple write. It becomes a state machine: show options, handle scrolling, handle confirmation, maybe forget a move, maybe cancel. So in your architecture, if your eligibility function directly edits the creature, your UI has nowhere clean to live. The FireRed-style approach lets menu code orchestrate the decision while helper routines perform narrow data checks and mutations.

Let’s use a concrete edge case. A party creature has four moves: Tackle, Growl, Ember, Quick Attack. The player uses TM35, which teaches Flamethrower. The species bitset says yes. The creature does not already know Flamethrower. There is no empty move slot. What should happen? The system should not simply overwrite slot four. It should enter the learn-move workflow. The player can choose to forget Tackle, Growl, Ember, or Quick Attack, or cancel. Only after that choice should the mutation occur.

Now change one condition. The same creature already knows Flamethrower. The bitset still says yes. Does the player get the forget-move UI? No, because duplicate prevention should happen before slot pressure. The creature already possesses the move. The correct response is a message, not a mutation workflow.

This duplicate issue also appears during level-up. Suppose Emberling already knows Ember because the player used a TM or some earlier special event. Then level 7 arrives and the level-up table offers Ember. You should not insert a duplicate Ember into another slot. In a four-slot move model, duplicate moves are normally disallowed. So the level-up applier should check current possession before adding a candidate. And again, eligibility is not possession. The level-up table may say Ember is available at level 7, while the creature record says Ember is already present. The resolver is not wrong. The applier simply decides no mutation is needed.

Let me ask a check question. If a level-up table contains two entries at the same level, and the first move is already known but the second is not, should the second move still be considered? Yes. Duplicate prevention should suppress that candidate, not abort the whole level’s processing. This is why your loop should process entries individually.

Now, let’s connect this to evolution, because evolution is one of the places where students tend to mix species identity and creature continuity. A creature that evolves changes species, but it is still the same individual record in the player’s party. That means its current moves are not wiped just because the species ID changes. Its stats may be recalculated because base stats come from the new species info. Its future eligibility changes because the species lookup now points at a different species. But its existing move slots remain part of the individual’s state unless a specific evolution event or later level-up event changes them.

This is where `pokefirered/src/data/pokemon/species_info.h` `chunk_2e759c52d0f4129a` matters for the lecture’s source focus. Species info is the species-side authority. It can feed stat calculation and other species properties. But current moves live in the creature record, and mutation and lookup helpers in `pokefirered/src/pokemon.c` are the runtime layer named in the packet. So after evolution, “what are my base stats?” and “what level-up moves are now available?” are species-indexed questions. “What moves do I currently have?” remains a creature-state question.

Here is an example. Emberling evolves into Flareon-like Blazeling at level 16. Emberling’s learnset has Ember at 7 and Quick Attack at 13. Blazeling’s learnset has Flame Burst at 16. When evolution occurs, you do not rebuild the move list from Blazeling’s level-one-to-sixteen history unless the game has a specific rule for doing that. You preserve the current moves. Then, depending on the engine’s sequencing, you may resolve moves available at the evolution level for the new species. The sequencing is a design contract. If your clone chooses level-up resolution before evolution, you may get one behavior. If it chooses evolution before a same-level learn check, you may get another. The commercial game has its own sequencing, but the architecture lesson is broader: sequencing should be explicit, testable, and not hidden inside the species table.

Let’s pause and recap the boxes. Current creature state: current moves, PP, level, species ID, and individual values. Species tables: base stats, growth rate, maybe learnset pointer, compatibility rows. Eligibility resolvers: answer “what could be learned now or through this item?” Mutation workflow: writes to move slots, initializes PP, handles duplicates, and invokes UI if full. These are separate boxes with arrows between them.

The summary screen gives us another useful anchor. The research packet cites `pokefirered/src/pokemon_summary_screen.c` `chunk_e8e17a42bbf17d46`, which includes Pokémon, battle, item, move, and menu-related dependencies. That tells you the summary screen is a reader and presenter of state. It needs to display moves and maybe PP, but it should not be the authority that determines species learnability. In a clean architecture, summary UI can inspect the creature and render current possession. TM use UI can inspect compatibility and render eligibility. Learn-move UI can coordinate mutation. Those are not the same screen responsibility.

And this carries forward to the overall course plan. Back in the runtime shell weeks, we studied callbacks and tasks: `AgbMain`, main callback slots, `SetMainCallback2`, task allocation, and per-frame dispatch through `RunTasks`. We do not need to re-teach that now, but we should use it. A move-learning menu is not just data. It is a small state machine living inside the callback/task-driven runtime shell. Back in the script weeks, we studied flags and variables as world memory. We do not need to re-teach script pointer execution, but we should use it. A script might award a Pokémon, trigger an evolution check, or give a TM, and those script-level actions eventually rely on the same creature mutation APIs. Back in the party data lectures, we studied `GetMonData`-style access. We do not need to reopen the representation, but we should remember why callers use accessors: because stored creature data is compact, structured, and protected by conventions.

So week 10 is not a side topic about moves. It is a bridge between data-driven design and runtime mutation. The same table-to-runtime-to-visible-behavior trace appears again. A row in `tmhm_learnsets.h` influences which party members are selectable for an item. A level-up entry influences whether a post-battle message appears. A move slot mutation influences battle commands, summary display, PP tracking, and save persistence. If you design this layer casually, the defects will show up everywhere.

Now let’s talk about testing, because at this point in the course the assessments have already checked system decomposition and minimal clone architecture. I am going to hold you to a higher standard than “it worked in the happy path.” Your three-species lab should have edge cases.

One species should have multiple moves at the same level. One species should be compatible with a TM but already know the move. One species should evolve into a species with a different compatibility row. One creature should have an empty move slot. One should have four moves. And one test should verify that level-up move resolution returns ordered candidates, not a set.

If you write only one test, write this one: old level 4, new level 6, learnset entries at 5 and 6, and the creature already knows the level 5 move. The expected behavior is that the level 5 duplicate is skipped and the level 6 move is still offered. That test catches three mistakes at once: checking only the final level, aborting on duplicate, and conflating resolver output with mutation outcome.

Notice I said old level 4, new level 6. Rare candy or experience gain can create level transitions, and your resolver must decide whether it handles one level at a time or a range. In many creature RPGs, if a creature jumps multiple levels, it should process all level-up opportunities in order. So the resolver signature should not be only species and current level unless your caller loops per level. You need either “resolve moves for exact level” plus caller sequencing, or “resolve moves between old and new level” with internal ordering. Both are valid if the contract is clear.

Let me put a warning on the board: hidden sequencing is a bug incubator. If `gainExperience` silently teaches moves, evolves, recalculates stats, opens UI, and updates party display all inside one opaque function, you cannot reason about edge cases. A FireRed-style C codebase does have large procedural flows, yes, but the important pieces are still separated by data tables, helper functions, menu state machines, and runtime callbacks. The packet’s clustering around `pokemon.c`, `learn_move.c`, and UI modules supports that reading.

We should also mention daycare briefly, not because we are teaching breeding today, but because daycare is another subsystem that can touch creature growth and moves. The research packet includes `pokefirered/src/daycare.c` `chunk_119931105f546346`, with dependencies reaching into battle, constants for moves and items, mail data, storage, event data, scripts, party menu, and more. That wide include surface is a signal: creature state is shared across systems. When a subsystem like daycare changes levels or affects moves, it must honor the same underlying rules or call the same helpers. Otherwise you get inconsistent behavior depending on where the level came from.

Here is the architecture principle in plain language: every path that can produce a new move should converge before writing the move slot. Level-up, TM/HM, move relearner, event gift, daycare, and evolution-related learning may have different eligibility rules. But once a move is to be added to a creature, the system should reuse the same possession check, empty-slot check, PP initialization, and full-moves workflow where applicable.

Now, another check for understanding. Suppose a TM teaches Water Pulse. Species A has the Water Pulse bit set in the TM/HM table. Species B has Water Pulse in its level-up learnset at level 20 but does not have the TM bit set. Can Species B learn Water Pulse from that TM? No. The move appearing in one eligibility channel does not imply eligibility in another channel. That is exactly why we model the channels separately.

And the reverse is also true. Species A might learn Water Pulse by TM but never by level-up. If it reaches level 20, nothing happens unless its level-up table says something happens. The TM compatibility bitset does not create level-up opportunities.

Let’s bring this back to the board phrase one more time: eligibility is not possession. Underneath that I’ll add: channel is part of eligibility. Level-up eligibility, TM/HM eligibility, relearner eligibility, and event eligibility are not interchangeable unless the game design explicitly makes them interchangeable.

In the next segment, we will use this architecture to walk through a more precise implementation plan for the lab. We will sketch the data definitions for three species, define a compact machine bitset, write the resolver contract, and decide where duplicate prevention belongs. Then we will connect the move resolver back to save data and battle command availability, because once a move is written into the creature record, battle does not care whether it came from a level-up table, a TM item, or a move relearner menu. Battle sees current possession. That is the whole point of keeping the boundary clean.

So, let’s pick up exactly there. We are going to move from the slogan into an implementation shape. And I want you to notice that the implementation shape is not just “make some arrays.” At this level, “make some arrays” is the least interesting part. The interesting part is deciding what question each array answers, what question each resolver answers, and what question the saved creature record answers.

Let me put three boxes on the board. First box: species definition. Second box: eligibility tables. Third box: individual creature record. Now, under species definition, I’ll write base stats, types, growth, abilities if your engine has them, and whatever static biological or design facts belong to the species. Under eligibility tables, I’ll write level-up learnset, TM/HM compatibility, relearner source, event source. Under individual creature record, I’ll write current moves, current PP, PP bonuses if modeled, level, experience, species, personality or identity fields if modeled. That distinction is the architecture.

In FireRed, we have already seen that runtime Pokemon calculations and mutation are centralized through Pokemon data helpers rather than random screens reaching directly into every byte of the boxed or party structure. That is why the source focus for today includes pokefirered/src/pokemon.c chunk_4573760bdda53819 for runtime calculations, chunk_f56e3a6588d98715 for data mutation and lookup, and chunk_c089f03b70501fda for creature state helpers. I am not asking you to memorize those chunk IDs as trivia. I am pointing at them because they support the architectural claim: move learning is not an isolated menu feature. It is part of the same creature state pipeline as stats, species identity, stored move slots, and battle-visible state.

Now, for the lab, we are going to intentionally build a tiny version of that pipeline. Three species. A species table. A stat calculator. A level-up move resolver. A TM/HM compatibility bitset. And I want the resolver contract to be explicit enough that nobody accidentally mixes eligibility with possession.

Let’s make a concrete example. Suppose our three test species are Embercalf, Cindermane, and Tideroot. Embercalf evolves into Cindermane. Tideroot is unrelated. Embercalf learns Tackle at level 1, Ember at level 7, Quick Attack at level 10. Cindermane learns Flame Wheel at level 16 and Roar at level 20. Tideroot learns Tackle at level 1, Vine Whip at level 6, Water Sport at level 9, and Razor Leaf at level 15. Then our TM list, very small for the lab, has TM01 Focus Punch, TM02 Dragon Claw, TM03 Water Pulse, TM04 Calm Mind, and HM01 Cut. We decide Embercalf can learn Focus Punch and Cut, Cindermane can learn Focus Punch, Dragon Claw, and Cut, and Tideroot can learn Water Pulse, Calm Mind, and Cut.

Now pause there. If I ask, “Can Embercalf use Cut in battle right now?” what is the answer?

Not enough information. Exactly. Because compatibility with HM01 Cut says Embercalf is eligible to be taught Cut by that channel. It does not say this individual Embercalf currently possesses Cut in one of its four move slots. Battle command availability comes from the current move slots and PP. We covered the move slot and PP foundation in the previous session, and I am deliberately leaning on it here instead of re-teaching it. Once the move is in the creature record, battle sees it as a possessed move. Battle does not need to ask whether the move arrived by level-up, TM, HM, event, breeding, relearner, or debugging tool. That boundary is what keeps the battle engine from turning into an encyclopedia of acquisition history.

Let’s do a check for understanding. If a Tideroot is level 20 and has never been taught TM03 Water Pulse, should the level-up resolver return Water Pulse?

No. Good. It should return moves in the level-up channel only. Tideroot’s TM eligibility for Water Pulse belongs to a different query. The level-up resolver may say, “At level 20, by level-up history, this species could have learned Tackle, Vine Whip, Water Sport, and Razor Leaf,” depending on whether you are resolving all prior level-up moves or only the move triggered at exactly the new level. But it should not smuggle in Water Pulse because Water Pulse is not part of that table.

That distinction is also why FireRed can support multiple user-facing paths into move mutation. The move relearner is a menu and state machine problem on top of creature data mutation. The packet’s excerpt from pokefirered/src/learn_move.c chunk_af3598df6f092723 explicitly identifies a move relearner state machine and names menu state transitions around CB2_MoveRelearner_Init and MoveRelearnerStateMachine. That is a UI and flow layer. It presents choices, handles scrolling, accepts input, and then eventually writes a move decision into creature state. But the existence of that state machine does not mean battle needs to know about the move relearner. The battle system only consumes the result: the creature’s current move slots and PP.

Now I’m going to write the resolver contract on the board in English first, not code. A level-up resolver answers: given a species and a level condition, which moves are available through level-up data? A TM/HM resolver answers: given a species and a machine ID, is that species compatible with that machine? A teach operation answers: given an individual creature and a chosen move, can we place that move into a current move slot, and if yes, how do we initialize or preserve PP state? Those are three different contracts. Do not merge them into one function called canLearnMove unless you are extremely clear about the channel parameter.

This is where advanced students usually get into trouble. They design a convenient boolean called canLearnMove(species, move), and at first it works. Then they add TMs. Then they add relearning. Then they add special tutor moves. Then they add form changes. Then they add event-only moves. Suddenly canLearnMove returns true for moves that should not be obtainable from the current UI, and false for moves that are already legally possessed from an older event. The function has no way to answer the question because the question was underspecified.

So, better names. CanLearnByLevelUp. CanLearnByMachine. CanRelearnFromHistory, if you model history. CanReceiveEventMove, if you model event distributions. HasMove, for possession. CanUseMoveInBattle, which usually depends on possession, PP, disable effects, encore-like state, choice locks, battle rules, and so on. The names should make illegal mixing feel awkward.

Let’s bring FireRed back in. The species table continuation in pokefirered/src/data/pokemon/species_info.h chunk_2e759c52d0f4129a supports the idea that species facts are table-driven. The TM/HM table named in the research packet, pokefirered/src/data/pokemon/tmhm_learnsets.h, is the corresponding high-signal FireRed source for machine compatibility. Those are static design data. Then pokemon.c, especially the packet’s pokemon.c chunks for calculation, mutation, and state helpers, is where those facts become runtime answers for individual Pokemon. That is the architecture we want you to imitate in the lab: static tables, query helpers, mutation helpers, and no UI code owning the core rules.

Now, let’s talk bitsets, because this is the one implementation detail that looks old-fashioned until you understand the constraints. A TM/HM compatibility table could be stored as a list of machine IDs per species. That is readable. It could be stored as a dictionary from species to arrays. That is what many scripting-heavy engines would do. FireRed, being a GBA-era C engine with strict memory and performance expectations, uses compact table-driven data. A bitset is a natural fit because the question “can this species learn this machine?” is boolean and indexed. If we have 64 machines, we need 64 bits per species. That is eight bytes. For hundreds of species, that is still cheap and cache-friendly. The query is also simple: compute the machine index, find the word or byte, mask the bit, and return true or false.

Board cue: I’ll draw eight boxes, each box one byte, and label the bits 0 through 63. Suppose TM03 Water Pulse corresponds to bit 2 if we use zero-based indexing. Tideroot’s compatibility byte would have bit 2 set. If HM01 Cut corresponds to bit 50, then Embercalf and Cindermane and Tideroot each have bit 50 set. The important engineering point is that the bit position is not the move ID. It is the machine slot ID. TM03 teaches Water Pulse in our simplified lab, but the bit is “compatible with machine 3,” not “compatible with move Water Pulse” in the abstract. In many games those coincide in practice because the machine item deterministically teaches one move, but architecturally the table answers machine compatibility.

Here’s a subtle example. Imagine a randomizer or a mod changes TM03 from Water Pulse to Ice Beam. If the compatibility bitset is defined as machine compatibility, Tideroot can still use TM03, and now TM03 teaches Ice Beam. If the compatibility table is defined as move compatibility, then changing the TM contents should not automatically preserve compatibility. Neither design is universally correct. FireRed’s table is naturally understood in the TM/HM machinery context, and for our lab we will be explicit: the bitset is machine compatibility, and the machine table maps machine IDs to move IDs.

Check for understanding: if our lab randomizes TM03’s taught move from Water Pulse to Swift, and Tideroot’s TM03 bit remains set, can Tideroot learn Swift from TM03?

In our lab contract, yes. Because the bitset says compatible with TM03. If you dislike that design, that is fine, but then you must change the contract and the table name. Do not keep the old table name and silently change the semantics.

Now duplicate prevention. Where does it belong? Suppose Embercalf already knows Ember, and a level-up event at level 7 tries to teach Ember. Or suppose a player uses a TM that teaches a move already in slot two. We need to avoid duplicate move slots in almost every Pokemon-like ruleset. But duplicate prevention is not eligibility. It belongs at the teach operation boundary, because it depends on the individual creature’s current moves. The species table cannot know whether this individual already has the move. The TM bitset cannot know it. The level-up learnset cannot know it. The teach operation can know it, because it receives the creature record.

So the teach operation should probably run a sequence like this. First, validate that the move source has already been authorized, or receive a trusted move choice from the resolver. Second, check whether the creature already has the move. Third, check whether there is an empty move slot. Fourth, if there is an empty slot, place the move and initialize PP from the move’s base PP and the creature’s PP bonus model. Fifth, if there is no empty slot, return a “needs replacement” result to the UI. The UI then asks the player which move to forget. Sixth, after replacement, write the move and update PP. In FireRed terms, the UI flows may vary, but the architectural lesson is supported by pokefirered/src/learn_move.c chunk_af3598df6f092723 for the menu state machine side and pokefirered/src/pokemon.c chunk_f56e3a6588d98715 for the data mutation and lookup side. Menu state and creature mutation cooperate, but they are not the same layer.

Let’s connect this to save data. A saved creature does not need to store “learned Ember by level-up at level 7” in a simple FireRed-style current-moves model. It stores the current move slots and associated PP data. When you save and reload, the battle engine can reconstruct command availability from possession. That is why the possession boundary is so important. The save system preserves current creature state. The species and eligibility tables remain in the ROM or program data. If you patch the ROM’s learnset table, a saved creature’s current moves do not automatically rewrite themselves. New learning opportunities may change, but existing possession is stored.

This point matters for evolution too. Embercalf evolves into Cindermane. What happens to moves? In the classic style we are studying, evolution changes species identity and therefore changes future species-based queries: stats, base data, future learnset, TM compatibility if it differs, evolution rules, maybe sprite, cry, Pokedex identity, and so on. But evolution does not erase the current move slots simply because the species ID changed. That would be an extremely disruptive design. So after evolution, this individual still possesses its current moves, while future eligibility questions may now use Cindermane’s tables. This is a clean example of species transition versus creature record continuity.

Let me ask it as a quick oral quiz. Embercalf at level 15 knows Tackle, Ember, Quick Attack, and Cut. It evolves into Cindermane at level 16. Cindermane’s level-up table includes Flame Wheel at level 16. Cindermane’s TM compatibility includes Dragon Claw, while Embercalf’s does not. After evolution, does the creature automatically forget Cut because we are now using Cindermane’s species table?

No. Current possession persists unless you explicitly design a purge rule. Does it become eligible for Dragon Claw by TM after evolution?

Yes, if the TM/HM resolver uses current species and Cindermane’s bitset includes that machine. Does it get a chance to learn Flame Wheel at level 16?

That depends on your event sequencing. If evolution happens as part of level-up processing, you must decide whether level-up move resolution checks pre-evolution species before evolution, post-evolution species after evolution, or both in a defined order. FireRed has concrete behavior here, but for the lab I care less about copying every edge case and more about making your sequencing explicit. The architecture lesson is: species transitions affect future table lookups, while creature record fields carry continuity.

Now, I want to contrast this briefly with a modern data modeling style, because the research packet names pokemon-showdown/data/learnsets.ts as a high-signal secondary source. We do not treat Pokemon Showdown as FireRed mechanics. It is a simulator and competitive rules data model, not the GBA game engine. But it is useful as a comparison because modern learnset modeling often needs to represent not only whether a species can learn a move, but by which generation, by which method, and under which legality constraints. That is a richer legality model. FireRed’s runtime needs are narrower: support the game’s acquisition channels, mutate current creature moves, and let battle consume current moves. So Showdown helps us name the general problem space, while FireRed remains our authority for this course’s engine architecture.

This is also a good moment to connect week 10 back to the full course decomposition. In week 1 and week 2, when we talked about the runtime shell, callbacks, tasks, and generated data, it might have felt distant from something as concrete as “Can my Pokemon learn Cut?” But now you can see the connection. The TM item screen is UI. The item selection flow is task or callback-managed runtime behavior. The map may gate HM use through overworld rules. Scripts may give the player a TM, set flags, or trigger gift creatures. Creature data stores current moves. Battle consumes current moves. Save preserves creature state. Link systems need a consistent serialized creature representation so traded or battled creatures carry their moves correctly. A move eligibility table is therefore not just a data table. It is a small static fact that participates in the entire RPG architecture.

Let’s do one more board trace. Player receives HM01 from an NPC. We will not re-teach script command handlers today; use that as prior foundation. The script mutates inventory and perhaps flags. Later, the bag UI selects HM01. The item system resolves HM01 to move Cut. The TM/HM compatibility resolver checks the current species bitset. If compatible, the teach operation checks the individual creature’s current moves. If there is room, it writes Cut into a move slot and initializes PP. The save system later serializes that creature record. In battle, the command menu sees Cut as one of the current moves and displays it if battle state allows. In the overworld, a field move check may see possession of Cut plus badge or flag requirements plus map object conditions. Notice how many systems we just touched: event scripting, inventory, UI, creature data, save, battle, and overworld. That is the course objective in miniature.

And this is why I am being strict about function boundaries in the lab. If your bag UI directly pokes move slot three because “the creature can learn it,” you have collapsed UI, eligibility, and mutation into one accidental procedure. It may work for the three-species exercise, but it will fight you the moment we add replacement prompts, move relearning, evolution, or link validation. If instead your bag UI asks the compatibility resolver, then asks the teach operation, then responds to the result, you have a pattern that scales.

Now I’ll give you a small implementation sketch in speech, and I want you to imagine the code without writing it yet. SpeciesInfo contains base stats and maybe a pointer or index to a level-up learnset. LevelUpMove is a pair: level and move ID. The table ends with a sentinel, or you store a length. TMHMLearnset is an integer bitset, maybe a 64-bit value for the lab. MachineInfo maps machine ID to move ID and item ID. A Creature contains species, level, moves array of four move IDs, PP array of four values, and maybe PP bonuses. ResolveLevelUpMoves takes species and level, and returns the move or moves whose learn level equals that level. ResolvePriorLevelUpMoves, a different helper, returns all moves at or below a level if you need party generation or relearning. CanLearnMachine takes species and machine ID, masks the bitset, and returns a boolean. TeachMove takes creature and move ID and returns success, already-known, needs-replacement, or invalid depending on how you design the API.

Check for understanding: why did I separate ResolveLevelUpMoves from ResolvePriorLevelUpMoves?

Because “what happens when I just reached level 10?” is not the same query as “what moves are historically available to this level 20 species?” The first is a level-up event query. The second is useful for generated wild creatures, NPC parties, relearner menus, or tests. If you use one function for both, you will probably add flags like includePriorMoves, includeCurrentLevel, includeEvolutionMoves, includeRelearner, and soon the call sites become unreadable. Separate helpers are not bureaucracy; they preserve meaning.

Now, a note about tests. For homework_w09 you already designed minimal clone versions of map loading, script execution, party data, battle turns, item use, and save persistence. So for week 10, I expect tests that cross a boundary, not just tests that a bit mask works. A weak test says Tideroot’s bitset has bit 2 set. A stronger test says using TM03 on Tideroot succeeds, writes the machine’s move into an empty slot, initializes PP, and makes that move appear in the battle command list after save and reload. Another strong test says using TM03 on Embercalf fails at the compatibility step and does not mutate the creature. Another says teaching a duplicate move returns already-known and leaves PP unchanged unless your design explicitly refreshes PP, which you would need to document.

One more subtlety before we move on. Level-up moves and TM moves are both move IDs at the moment of possession, but they have different audit paths before possession. This matters for debugging. If a player reports that Cindermane cannot learn Dragon Claw by TM, you should know where to look: machine table, species TM/HM bitset, item flow, compatibility resolver, teach operation. If a player reports that Cindermane did not learn Flame Wheel at level 16, you look at level-up table, evolution sequencing, level-up event processing, move-slot replacement UI, and mutation result. Same final field, different upstream path.

That is the practical payoff of the architecture. It reduces debugging from “somewhere in Pokemon code” to a constrained trace. We have been practicing that trace all semester: data record, runtime global or creature struct, callback or task or script, visible behavior. The move eligibility system is one of the cleanest places to see it because the tables are small, the player-facing result is obvious, and the boundary between eligibility and possession is easy to violate.

So, as we go into the lab design, keep the board phrase in view: eligibility is not possession, and channel is part of eligibility. The lab is not complete when a species can “learn a move” in the abstract. It is complete when level-up eligibility, machine eligibility, current move possession, duplicate prevention, PP initialization, save persistence, and battle command availability each have a clear owner. In the next part of the lecture, we will take that owner map and turn it into the exact three-species lab contract, including the edge cases I want your tests to pin down.

Now, let’s convert that owner map into a lab contract precise enough that your implementation cannot hide ambiguity. I’m going to be fussy here because at this point in the course, vague creature systems are no longer acceptable. In week six, vague decomposition was tolerable as a sketch. By week ten, after you have already implemented map loading, script execution, party data, battle turns, item use, and save persistence in miniature, a phrase like “the creature learns the move” is not a design. It is a sentence covering at least five different responsibilities.

So, on the board, I’m going to draw three columns. First column: species data. Second column: runtime creature record. Third column: interaction channel. And under interaction channel I’m writing level-up, machine use, relearner, battle, summary, and save/load. Now, notice something about that set. Those are not all the same kind of thing. Level-up and machine use are acquisition channels. Battle and summary are consumers of already-owned move data. Save/load is persistence. Relearner is a filtered menu over historical eligibility, not just another copy of the machine table. This is exactly why the FireRed source is useful pedagogically. It does not let us pretend there is one magical “can learn” function that handles every player-facing question.

When we look at the FireRed source packet, the move relearner is explicitly a UI state machine, not a species table. The excerpt from pokefirered/src/learn_move.c, chunk_af3598df6f092723, even labels the “Move relearner state machine” and lists callback-style phases like CB2_MoveRelearner_Init and MoveRelearnerStateMachine. That is a strong architectural clue. The relearner is not where base compatibility belongs. It is a menu and workflow over data that must already exist elsewhere. Similarly, the summary screen pulls in pokemon.h, data.h, constants/moves.h, party_menu.h, and battle-related headers in pokefirered/src/pokemon_summary_screen.c, chunk_e8e17a42bbf17d46, because the summary screen is a consumer and presenter of creature state. It should not be secretly deciding whether a TM is valid. It should display what the creature currently has, with PP, ordering, and user-facing formatting.

Let me make that concrete. Suppose our lab has three species: Embercalf, Reedfin, and Voltbat. Embercalf is Fire-like, Reedfin is Water-like, Voltbat is Electric-like. I do not care whether you use those exact names, but I do care that your test set has overlapping and non-overlapping compatibility. If every species can learn everything, your tests prove almost nothing. So Embercalf might learn Tackle at level 1, Ember at level 7, and Flame Wheel at level 15. Reedfin might learn Tackle at level 1, Bubble at level 5, and Water Pulse at level 14. Voltbat might learn Quick Attack at level 1, Thunder Shock at level 6, and Spark at level 13. Then for machines, maybe TM01 Focus Punch is compatible with Embercalf and Reedfin but not Voltbat. TM24 Thunderbolt is compatible with Voltbat and maybe Reedfin if you want a surprise cross-type case, but not Embercalf. HM03 Surf is compatible only with Reedfin. This gives you enough shape to test yes, no, overlap, and progression.

Now, pause there. What is the difference between Embercalf learning Ember at level 7 and Embercalf being compatible with TM01? The level-up table says, “At or by this level, this species has this move in its natural progression.” The TM/HM bitset says, “If the player presents this external item or machine channel, this species is eligible for that move.” Neither statement says, “This individual creature currently has that move in slot two.” That current possession belongs to the runtime creature record, the same conceptual zone we have been building around Pokemon and BoxPokemon representation and GetMonData-style access. We are not reteaching that representation today, but we are using it. Your lab creature needs move slots and PP fields that are mutated when acquisition succeeds.

Here is the trap I expect some of you to fall into unless we name it now. You will write a function called canLearnMove(species, move), and then you will use it for everything. That function will pass your first two tests and then quietly damage the architecture. Why? Because “can learn” is channel-sensitive. Can Reedfin learn Surf by level-up? No, maybe not. Can Reedfin learn Surf by HM? Yes. Can it relearn Surf from the move relearner? No, if Surf was never in its level-up history. Can it use Surf in battle? Only if it currently has Surf in one of its move slots and enough PP, ignoring special battle constraints we are not modeling this week. Same move, same species, four different answers depending on channel.

Let’s do a quick check for understanding. If a level 20 Reedfin currently has Tackle, Bubble, Water Pulse, and Quick Attack, and the player uses HM03 Surf on it, what table do we consult first? Not the current move list. First we consult the machine compatibility bitset for Reedfin. If that says no, the attempt fails before move replacement. If it says yes, then we check whether Reedfin already has Surf. If it already has Surf, duplicate prevention should stop us from adding another Surf. If it does not have Surf and there is an empty move slot, we add Surf and initialize PP from the move data. If there is no empty move slot, we enter the replacement workflow. In your lab, you do not need to build the full replacement UI unless the assignment variant says so, but your resolver must return a state that makes that workflow possible. It cannot just silently overwrite slot zero.

I’m going to write that as a little board trace, and I want you to watch the ownership boundaries. Machine item selected. Party creature selected. Species read from runtime record. Species indexes TM/HM bitset. Move id comes from machine item mapping. Existing move slots checked for duplicate. Empty slot searched. PP initialized from move metadata. Creature record mutated. Save system later persists the mutated record. Battle later reads the mutated record. The order matters less than the ownership, but a wrong order often reveals a wrong ownership model.

FireRed’s table style reinforces this. The source focus for today includes pokefirered/src/data/pokemon/tmhm_learnsets.h as the high-signal FireRed TM/HM learnset table, and the important idea is that compatibility is stored as compact species-indexed data, not as ad hoc per-item script branches. That means the machine item workflow can be generic. A TM does not need a special script saying “if Bulbasaur then yes, if Charmander then no” in the overworld script engine. The item system can ask the creature data layer a clean question: for this species, is this machine bit set? This is the same data-table-to-visible-behavior pattern we have traced since the runtime shell and map weeks. A table entry becomes a runtime decision, which becomes a UI message and a state mutation.

Now, compare that with modern Pokemon Showdown only as a modeling contrast. The packet names pokemon-showdown/data/learnsets.ts as a high-signal modern learnset modeling file, but we should not treat it as FireRed mechanics. Showdown is solving a different problem: competitive legality across generations, formats, events, transfers, and simulator constraints. FireRed is implementing a retail game’s runtime and data-driven acquisition workflows. So, if you look at Showdown and see a rich learnset object, do not import that complexity wholesale into this lab. The lesson to take is that learnsets are structured legality data. The lesson not to take is that FireRed’s cartridge-era TM/HM eligibility must be represented in the same modern format.

Let’s refine the lab contract. Your species table must contain base stats, typing if your stat or move display needs it, a level-up learnset, and a machine compatibility bitset. We covered stat inputs last time, so I’m only invoking them as continuity: species base stats, individual values if your lab includes them, effort or simplified training values if configured, level, and nature only if your variant already added it. Do not let stat calculation and learnset resolution become one module. They both read species data, yes, but they answer different questions. In the FireRed packet, pokefirered/src/pokemon.c chunk_4573760bdda53819 is identified with Pokemon runtime calculations, while chunk_f56e3a6588d98715 is identified with data mutation and lookup, and chunk_c089f03b70501fda with creature state helpers. Even from those source labels, you can see the separation we want you to preserve: calculate derived battle-facing numbers, mutate or look up stored data, and provide helpers over creature state. Your lab does not need to mirror FireRed’s exact function boundaries, but it needs that discipline.

For the level-up resolver, I want two operations, not one. First, given species and level, return the set or sequence of moves that should be considered newly learnable at exactly that level. Second, given species and current level, return the historical moves available up to that level if you are implementing a relearner-like menu. Those are different operations. If you collapse them, you will accidentally offer future moves, or you will fail to handle a creature obtained above level one. Think of a newly created level 10 Reedfin. Does it start with only its level 10 move? No, normally initial move construction scans the level-up history up to that level and fills a legal recent set, subject to slot limits. But when it levels from 9 to 10, you only prompt for moves at the new level. Historical initialization and level transition are not the same event.

Here is a concrete edge case. Voltbat has Quick Attack at level 1, Thunder Shock at level 6, and Spark at level 13. You create a level 13 Voltbat from a test fixture. What are its moves? If your fixture constructor says “give all moves learned up to level 13,” that is three moves, in learned order. If there were five moves by level 13, you need a policy: keep the most recent four, keep the earliest four, or require explicit fixture moves. FireRed-style party creation has its own rules and utilities, but for our lab, I want the policy written down and tested. My recommendation is most recent four for automatic initialization, because it resembles the idea that newer level-up moves replace older ones over time. But if your lab handout variant says explicit fixture moves only, follow that. The architectural requirement is not which policy you choose; it is that the policy lives in the creature creation or level-up subsystem, not in battle display.

Let me ask another check question. If Voltbat is compatible with TM24 Thunderbolt, and Thunderbolt is not in Voltbat’s level-up table, should the move relearner show Thunderbolt after the player has used the TM once and later deleted the move? Careful. In the simplified lab, probably no, unless you explicitly model remembered machine moves. In FireRed’s move relearner context, pokefirered/src/learn_move.c, chunk_af3598df6f092723, is a dedicated relearner menu over eligible remembered moves, not a universal archive of every move ever possessed. Modern games may have different memory policies; do not backport them accidentally. This is why we keep acquisition channel in the data model. “Once knew this move” and “species historically learns this move by level” are not automatically the same predicate.

Now let’s talk about the bitset itself, because advanced students should not just store a list and call it a day without thinking about representation. In a small lab, a set of move ids is acceptable, but I want you to implement the TM/HM compatibility as a bitset or bitmask because that is the point of this exercise. The representation teaches a real cartridge-era design pressure: many species, many machines, small fixed eligibility answers. A bitset makes membership cheap and table storage compact. If you have 8 machines, one byte per species is enough. If you have 64 machines, eight bytes per species. The operation is mechanical: machine index maps to a bit position, species row gives you the mask, and compatibility is mask AND one-shifted index nonzero. But the design consequence is bigger: the move id is not the bit position unless you choose to make it so, and in a TM/HM system you usually should not. TM24 maps to Thunderbolt. The bit position maps to “machine slot 24,” not to MOVE_THUNDERBOLT as a global move number.

That distinction gives us another edge case. Suppose your move constants are TACKLE equals 1, EMBER equals 2, SURF equals 57, THUNDERBOLT equals 85, but your machine list has only three entries in the lab: TM01 Focus Punch, TM24 Thunderbolt, HM03 Surf. If you shift by move id, you will build a sparse and fragile compatibility mask. If you shift by machine index, your mask is compact and correct. So the item layer or machine table must translate item id to machine index and move id. The species compatibility table answers only the machine index question. The move data table answers PP, type, power, and display name. The creature record stores move ids and current PP. Again, three columns on the board: species data, runtime creature record, interaction channel.

We should also pin down PP initialization. When a creature acquires a move, current PP should be initialized from the move’s base PP, possibly modified by PP Up-like mechanics if your lab includes them, but most of you will not include those this week. Do not initialize PP to zero and expect battle to fix it. Battle is not the owner of acquisition. In FireRed’s architecture, battle setup and battle systems include pokemon.h and move constants because they consume creature state for battle entry and execution; the packet excerpt from pokefirered/src/battle_setup.c, chunk_494d132a59422c85, shows battle setup pulling in pokemon.h, item.h, script.h, event data, battle headers, and transition code. That is the integration point from overworld into battle. But integration is not license to mutate basic move acquisition rules inside battle. Battle command availability should be derived from the creature’s current moves and PP at battle time.

Let’s run the full example out loud. Player selects HM03 Surf from inventory. The item system knows HM03 corresponds to machine index two in our three-machine lab and move id Surf. It opens party selection. The player selects Reedfin. The resolver reads Reedfin’s species id from the runtime creature record. It reads Reedfin’s machine mask from the species table. The Surf bit is set, so the species is eligible by this channel. The resolver scans Reedfin’s four move slots. It does not find Surf. It finds no empty slot because the current moves are Tackle, Bubble, Water Pulse, and Quick Attack. The resolver returns “eligible but replacement required,” carrying move id Surf and maybe base PP. A UI workflow asks which move to forget. The player chooses Tackle. Only then does the mutation function replace Tackle with Surf and set current PP to Surf’s base PP. The save system eventually serializes the changed creature record. Later, in battle, the battle UI lists Surf because it is in the move slot, not because Reedfin remains HM-compatible.

Now change one variable. Player selects Embercalf for HM03 Surf. Same item, same move id, same machine index. Species mask does not contain the Surf bit. The resolver returns “not compatible.” It should not inspect empty move slots as if an empty slot made the creature eligible. Empty capacity is not eligibility. It should not ask battle whether Surf is a Water move and Embercalf is Fire-like. Type is not machine compatibility unless your data table encodes it. And it should not mutate anything. Your test should assert all three: failure result, unchanged move list, unchanged PP.

Another variable. Reedfin already knows Surf. The resolver sees compatibility yes, duplicate yes, and returns “already knows.” This is not the same as incompatible. A good UI might say “Reedfin already knows Surf” rather than “It won’t have any effect” or “Not able.” Even in a small lab, keep result codes distinct. Architecturally, distinct result codes are cheap and they stop you from encoding user-facing meaning as fragile strings. The UI layer can translate result codes into messages. That pattern should feel familiar from script command handlers and flags, but I am not going to reteach those systems. We are just carrying the same discipline forward: state and reason are represented, then UI presents them.

For level-up, the edge cases are slightly different. Suppose Embercalf levels from 6 to 7 and learns Ember at 7. If it has an empty slot, add Ember and initialize PP. If it already knows Ember somehow, maybe from a starting fixture or a special event, do not duplicate it. If it has four moves, return replacement required. If the player declines replacement, the creature remains unchanged, and the level remains gained. Move learning is a consequence of level-up, not a condition for level-up success. That last sentence matters. Do not roll back experience or level because the player declined a move. Your tests should make that explicit.

Now, what about multiple moves at the same level? This is where many simple implementations break. Real learnset tables can contain more than one move for a level. Your three-species test set should include at least one species with two moves at the same level, even if it feels artificial. For example, Reedfin at level 10 learns Aqua Ring and Bite. When leveling from 9 to 10, the resolver should produce two acquisition prompts in deterministic order. If the first move fills the last empty slot, the second should require replacement. If the player declines the first and accepts the second, the final state should reflect that. This tests whether your level-up resolver returns a sequence of learn events rather than a single nullable move.

Let me connect this back to the broader course architecture. In the first weeks, we treated the runtime shell as the conductor: main callbacks, task allocation, per-frame dispatch. In the overworld weeks, we saw maps and scripts feeding visible behavior through event state. In the creature weeks, we moved into packed records, species tables, and GetMonData-style access. Today’s learnset system is not isolated trivia. It is a small slice through the whole game: inventory initiates machine use, UI selects a party member, creature data checks species eligibility, mutation changes the party record, save persists it, battle consumes it, summary displays it, and link systems must eventually serialize or validate comparable creature state. This is why the objective says decompose a complete creature-collector RPG into runtime shell, overworld maps, event scripting, creature data, battle, inventory, UI, save, and link systems. Move eligibility touches nearly all of them without being owned by all of them.

I’ll give you the lab acceptance shape verbally, since the written handout will use the same terms. You need three species with base stats and level-up learnsets. You need a move table with at least names and base PP. You need a machine table mapping machine item or machine id to move id and compatibility bit index. You need a per-species TM/HM bitset. You need a runtime creature record with species, level, current moves, and current PP. You need functions that answer level-up moves at a level, historical moves up to a level if implementing relearner behavior, machine compatibility by species and machine, acquisition attempt results, and actual mutation after replacement choice. And you need tests for incompatible machine, compatible machine with empty slot, compatible machine requiring replacement, duplicate prevention, level-up single move, level-up multiple moves at same level, and save/load preserving the learned move and PP.

One final check before we move into implementation strategy. If a saved Reedfin knows Surf, and then you change the species compatibility table so Reedfin is no longer compatible with HM03, should loading the save delete Surf? In our architecture, no. Possession is not recomputed from eligibility on load. The saved creature record says it currently has Surf. Compatibility controls future acquisition through that channel. Now, a tournament validator or link legality checker might ask a separate legality question later, especially in a modern simulator-like model, but that is not the ordinary load path for our FireRed-style lab. This distinction is one of the big maturity markers in your design: runtime state is not merely a cache of current tables.

So as you start coding, keep your tests arranged around questions, not around files. “Can this species use this machine?” “What happens when this creature gains this level?” “What exact mutation occurs when the player accepts replacement?” “What does battle read afterward?” “What survives save/load?” If you can answer those without crossing ownership boundaries, your implementation will be small and sturdy. If your battle menu starts checking TM bitsets, or your species table starts storing current PP, or your inventory item directly edits a move slot without a resolver result, stop and redraw the three columns. The architecture is telling you where the mistake is.

In the next segment, I’m going to walk through a reference implementation shape, not full code yet, but the signatures and data layout I expect. We will start with the machine index problem because that is the one that most often looks harmless and then infects every test with off-by-one assumptions.

So, machine index problem. Let me draw the first little table on the board. In the left column I’m writing item identity: TM01, TM02, HM01, and so on. In the middle column I’m writing move identity: Focus Punch, Dragon Claw, Cut, whatever the local move constant is. In the right column I’m writing eligibility index: zero, one, two, three. Now pause there, because this is the trap. Those three things feel like they ought to be the same number, or at least close enough that you can improvise. They are not the same architectural role.

The inventory item is what the bag owns. The move identity is what the battle engine eventually executes. The compatibility index is what the species table compresses into a bitset. If your lab implementation lets those collapse into one integer, it may pass the first happy-path test, but it will become brittle the moment you add HMs, a reordered item list, or a compatibility table that deliberately excludes non-machine moves.

This is why FireRed’s design is data-driven but not shapeless. The TM/HM learnset table named in the research packet, pokefirered/src/data/pokemon/tmhm_learnsets.h, is not a list of current moves on a creature. It is species capability data. The runtime creature record asks questions through lookup and mutation helpers in pokefirered/src/pokemon.c, especially the chunks we have been using this week: chunk_f56e3a6588d98715 for data mutation and lookup, chunk_c089f03b70501fda for creature state helpers, and chunk_4573760bdda53819 for runtime calculations. The point is not that every function is glamorous. The point is that the architecture keeps asking, “which owner has authority over this fact?”

Now, suppose we are designing the lab’s three-species test set. I would like you to imagine something intentionally small. Species A is a starter creature. Species B is its evolved form. Species C is unrelated. We define four moves: Tackle, Ember, Vine Hook, and Surf. We define two machines: TM01 teaches Ember, HM01 teaches Surf. If Species A can learn TM01 but not HM01, Species B can learn both, and Species C can learn neither, the table should encode exactly that. It should not say Species A currently knows Ember. It should not say Species A has five PP remaining for Ember. It should not say the player owns TM01. Those are separate systems.

Let me say that again in implementation terms. The species table answers “can this species ever learn this machine?” The party creature answers “what move slots are currently occupied, and what PP values are stored?” The bag answers “does the player have the teaching item available?” And the UI answers “how do we present the result and ask for confirmation?” If you merge those questions, you erase the boundaries we have spent ten weeks building.

A useful signature for the lab is something like CanSpeciesLearnMachine(species, machineIndex). It returns a boolean. Do not pass the whole creature unless the rule actually needs creature state. In FireRed’s generation of mechanics, basic TM/HM compatibility is species-based. There may be context around eggs, forms, or special cases in other games, and this is where Pokemon Showdown becomes a useful comparison but not an authority for FireRed. The research packet calls out pokemon-showdown/data/learnsets.ts as a high-signal modern learnset modeling file, but we should treat it as a different modeling target. It is excellent for seeing how a simulator represents broad move provenance across generations. It is not a reason to retrofit FireRed’s TM/HM path into a modern tagged learn-source system.

Now look at the board. I’m writing the bitset as binary for the lab: Species A has 01, Species B has 11, Species C has 00. If bit zero is TM01 and bit one is HM01, then eligibility is a shift and mask. That is the whole point of the representation. It compresses many yes/no questions into a compact field. In a large species table, this is not just about memory, although memory matters on the GBA. It is also about making the common question uniform. “Can species X learn machine Y?” becomes table lookup, bit extraction, return boolean.

Check your understanding here. If the player uses TM01 on Species A, which table tells us whether the move is compatible? Right, the species compatibility bitset. Which record tells us whether Species A already has four moves? The creature’s current move slots. Which table tells us what move TM01 maps to? The machine-to-move mapping, not the species table. Which system removes or consumes the item, if the design consumes it? Inventory. Good. Those answers are boring in the best possible way. Boring means the ownership is correct.

Now let’s walk the use case in runtime order. The player selects a TM in the bag. The inventory UI knows the item. It does not yet know which party member will receive it, so it transitions into a party-selection or compatibility-preview flow. The party menu asks, for each creature, “what is your species?” It can get that through the GetMonData-style access layer we covered in the creature records lecture; I’m not going to reteach that layer, but remember that it is there to prevent random modules from digging into encrypted or packed creature storage. Once the species is known, the TM/HM resolver converts the item to a machine index and a move id, checks the species bitset, and reports compatibility. The UI renders “able” or “not able,” or whatever local text the game uses.

At no point should the summary screen or party menu invent compatibility rules. The summary screen has plenty of work already. The packet’s excerpt from pokefirered/src/pokemon_summary_screen.c, chunk_e8e17a42bbf17d46, shows it sitting at an intersection of pokemon.h, battle headers, constants for moves and items, menus, link, and region data. That file is a great reminder that UI modules necessarily import many concepts because they display the game’s state. But importing constants to display a result is not the same as owning the rule. Advanced codebases rot when display modules quietly become rule engines.

Now, after compatibility passes, the next question is capacity. Does the creature have an empty move slot? If yes, the mutation can be straightforward: write the new move id into the empty move slot and initialize PP from the move’s base PP, maybe adjusted by PP Ups depending on the generation and exact helper path. If no, we enter replacement flow. This is where the earlier lecture on move slots and PP matters. A move slot is not just a move id. It travels with PP state and PP bonus state. If you replace a move, you must define precisely what happens to the old move id, its PP, and its PP Ups. A sloppy implementation will update the move id but leave stale PP, and then your battle engine will read impossible state.

The FireRed source structure supports this separation. The pokemon.c chunks in this week’s focus, especially pokefirered/src/pokemon.c chunk_f56e3a6588d98715 and chunk_c089f03b70501fda, are the right mental home for mutation and helper operations over creature data. Learn-move UI state is elsewhere. The packet’s pokefirered/src/learn_move.c chunk_af3598df6f092723 explicitly frames a “Move relearner state machine” with callbacks, menu scroll positions, and state transitions. Even though the move relearner is not identical to using a TM, the architectural lesson is the same: the menu flow can be a state machine, but the legitimacy of the final creature mutation still belongs to the creature data helpers and move-learning rules.

Let’s do a concrete example. Species A is at level 15 and currently knows Tackle and Growl, with two empty slots. The player uses TM01, which maps to Ember, machine index zero. The inventory UI says, “selected item TM01.” The resolver says, “TM01 means machine index zero and move Ember.” The species table says, “Species A bitset is 01, so bit zero is set.” The creature state says, “slot zero has Tackle, slot one has Growl, slot two is empty, slot three is empty.” The mutation chooses the first empty slot, writes Ember there, initializes Ember’s PP, and leaves Tackle and Growl alone. Afterward, battle does not care that Ember came from a TM. Battle reads the move slot and PP. That is exactly the kind of data-to-visible-behavior path we have been tracing since the runtime shell weeks: table, resolver, state mutation, callback-driven UI, then later battle behavior.

Now change only one variable. Species C has bitset 00. The player uses TM01 on Species C. The resolver still maps TM01 to Ember. The item still exists. The party member still exists. But the species check fails. No move slot is inspected for mutation, no PP is initialized, and inventory should not consume the teaching item. If your code removes the item before checking species eligibility, you have put side effects too early. If your code opens the replacement menu before checking compatibility, you have confused capacity with legality. If your code writes Ember and then later decides it was illegal, you have made rollback necessary where no rollback should be needed.

That ordering is worth writing down. Board cue: I’m drawing a narrow vertical sequence now. Item selected. Resolve item to machine and move. Get creature species. Check compatibility. Check whether move is already known. Check capacity. If empty slot, mutate directly. If full, ask replacement. If replacement accepted, mutate. Then return to UI and inventory consequences. The exact placement of item consumption depends on the mechanic, but it must be after the player’s successful acceptance, not before eligibility.

The “already known” check is another small but important architectural decision. If a creature already knows Ember, should the TM be usable? Usually the answer is no meaningful mutation should happen; the UI should report that the creature already knows the move. That check is not the same as compatibility. Species A may be compatible with TM01 and still be an invalid target for this specific use because it already knows Ember. In other words, eligibility has layers. Species eligibility is durable and table-driven. Current learnability is runtime and creature-specific. UI availability is the composition of bag state, species table, current move slots, and player confirmation.

This is where advanced learners should resist the temptation to make one giant TeachMove function that takes every global and returns every possible message. You want a small rule stack. One function maps item to machine metadata. One function checks species compatibility. One function checks whether the move is already present. One function finds an empty slot. One function performs the replacement mutation. The user-facing state machine can call these in order and choose text. That gives you tests that say, “Species C cannot learn TM01,” separately from tests that say, “a full moveset requires replacement,” separately from tests that say, “replacement resets the new move’s PP correctly.”

Let me connect this back to evolution for a moment, because tomorrow’s work and the rest of week 10 depend on it. Evolution changes species identity. If TM/HM compatibility is species-based, then evolution changes future compatibility. It does not automatically rewrite the current move slots, unless the evolution flow separately teaches a move or triggers a level-up move check. So if Species A cannot learn HM01 and Species B can, the same individual creature may become compatible after evolution because its species field changes. That is not a contradiction. The creature’s identity has transitioned. The table query now points at a different row.

But be careful. Existing moves are historical state. If Species A learned Ember by TM before evolving, and Species B’s bitset somehow did not include TM01, you would not delete Ember on evolution. Compatibility tells you whether a teaching event is allowed now. It is not a validator that constantly purges current moves. This distinction matters for trades, events, breeding, migration, and all the strange edge cases that accumulate in a long-running RPG. A creature record is not a pure projection of its species row. It is a persistent individual with a species id, level, stats, moves, PP, experience, personality-derived details, and other fields. The species table supplies defaults and rules; it does not replace individual history.

Now, for level-up learnsets, the shape is different from TM/HM bitsets. A level-up learnset is not usually “can this species learn move X by any level?” as a bitset. It is an ordered or sentinel-terminated sequence of level and move pairs. The resolver asks a temporal question: “At this level transition, what move or moves become available?” That question depends on the species and the new level, and sometimes on whether multiple levels were crossed at once. In a simple lab, if Species A learns Ember at level 7 and Quick Attack at level 10, then gaining from level 6 to level 7 should offer Ember. Gaining from level 6 to level 10 may need to offer Ember and Quick Attack in order, depending on the mechanic you choose for the lab. In FireRed-like behavior, you should think in terms of level-up events and learn-move prompts, not a passive synchronization of moves with level.

Here is the concrete lab resolver I expect you to be able to write without drama. Given species and newLevel, return the moves whose learn level equals newLevel. That is the minimal version. Then, for robustness, add a second version for rare candy or experience jumps: Given species, oldLevel, newLevel, return every move with oldLevel less than learnLevel and learnLevel less than or equal to newLevel, ordered by level and then table order. Even if the final FireRed path has more nuance in where the loop occurs, this lab structure teaches the correct data ownership. The learnset table stores facts. The leveling routine detects a transition. The move-learning UI handles acceptance and replacement. The creature mutation helper writes the result.

Check for understanding. Why is a level-up learnset not represented as the same kind of bitset as TM compatibility? Because we need level thresholds and ordering. A bitset can answer membership cheaply, but it cannot by itself tell us the level at which the move is offered or the order in which multiple moves should be processed. Good. And why is the current move list not recomputed from the level-up table every time the creature levels? Because the player may decline moves, replace different moves, learn TMs, learn HMs, use tutors, or come from another source. Current moves are persistent individual state.

This is also where Pokemon Showdown’s data model is pedagogically interesting. The packet highlights pokemon-showdown/data/learnsets.ts as high-signal, and that file represents learnability for a simulator that needs to validate formats across many generations and sources. A modern simulator often cares about provenance tags: level-up, machine, tutor, egg, event, transfer, and so on. That is a broader validation problem than FireRed’s in-game teaching flow. So use Showdown as a comparison for the general idea that learnsets are declarative data, but keep FireRed as the authority for the in-game architecture we are modeling.

Let’s bring battle back in, briefly. Battle should be downstream of all of this. When a battle starts, setup code gathers party state and transitions from the overworld into battle. The research packet’s pokefirered/src/battle_setup.c chunk_494d132a59422c85 shows battle setup importing pokemon.h, script, overworld, items, strings, and battle transition machinery. That is the boundary where the world hands a party into a different runtime mode. Once in battle, move execution should operate on the moves currently present. The battle engine should not ask, “was this move legal by TM?” during ordinary move selection. Legality was handled at acquisition time. This is a crucial architectural simplification. You validate at the mutation boundary so the hot runtime path can trust stored state.

Now, yes, a defensive simulator or online validator may revalidate more often. Link battles may need additional checks against malformed data. Save loading may sanitize corrupted records. But those are separate concerns. The normal single-player battle loop should not duplicate the TM/HM compatibility matrix. Duplicating it creates divergence: maybe the bag says a creature can learn Surf, the mutation writes Surf, and then battle refuses to show Surf because someone copied an old compatibility rule. That kind of bug feels mysterious to players because the game contradicts itself. Architecturally, it is just two owners claiming one fact.

Let me give you a test design that catches this. In your lab, create Species B with bitset 11 and level-up learnset containing Ember at level 7. Give a level 6 Species B four moves. Level it to 7. The resolver should return Ember. The UI layer should require replacement. Simulate replacing slot two. Assert that slot two is Ember and its PP is initialized. Then use HM01, Surf, on the same Species B. Compatibility passes. If the moveset is still full, replacement is required again. Replace slot three. Assert that slot two remains Ember and slot three is Surf. Now enter a fake battle-read function that simply lists current move ids and PP. It should see Ember and Surf with no knowledge of how they were learned. That test connects week 10 directly to the full course decomposition: creature data, inventory item use, UI confirmation, mutation, and battle readout.

Notice what we did not need in that test. We did not need an overworld map. We did not need script pointer execution. We did not need flag storage, except perhaps in a full game event around receiving the TM. Those systems are prior foundations, and we will use them when we place TMs in the world or gate HMs behind badges, but they are not part of the core move eligibility rule. This is how you keep a clone implementation small. You can still trace the complete RPG architecture, but you do not drag every subsystem into every unit test.

Now I want to say something about HMs because they tempt students into special-case chaos. In player-facing design, HMs are special: they often interact with field moves, badges, obstacles, and story progression. Architecturally, that does not mean the compatibility bitset needs to know about trees, boulders, or water tiles. Compatibility says whether the species can learn the move. Field use says whether a creature that currently knows the move can invoke an overworld action under the current progression rules. That field-use check belongs nearer the overworld, script, badge, and event-variable systems we covered earlier. If Cut removes a tree object, that behavior ties into maps and event state. But the fact that a species can learn Cut belongs in creature species data.

So if you are building HM01 Surf in the lab, do not implement surfing the map. Just mark the move as an HM machine, allow compatible species to learn it, and maybe mark it non-forgettable if you are modeling that rule. Non-forgettable status is a removal rule, not a compatibility rule. A move deletion interface would ask, “may this current move be forgotten?” and HMs may answer differently. Again, small predicates, clear owners.

Board cue: I am drawing four predicates now. CanSpeciesLearnMachine. CreatureKnowsMove. FindEmptyMoveSlot. CanForgetMove. These are not interchangeable. The first looks at species capability. The second looks at current individual state. The third looks at storage capacity. The fourth looks at deletion policy. A full teaching flow composes them, but none of them should secretly perform the whole flow.

Let’s pause for a quick verbal exercise. I’ll give you a scenario, and you name the owner. The player has not yet earned the badge required to use Surf outside battle. Owner? That is progression state, probably flags or variables plus field-use logic. A Wartortle-like species can learn Surf. Owner? Species TM/HM compatibility table. This individual currently knows Surf in slot one. Owner? Creature record. Surf has 15 base PP. Owner? Move data. The player selects Surf in battle and spends one PP. Owner? Battle mutates current PP through creature battle state and then persists the appropriate result back, depending on the engine’s state model. Good. That is the decomposition we want.

Now, because this is an advanced course, we should also talk about indexing and generated constants. In a hand-written lab, you might use an enum for machines: MACHINE_TM01, MACHINE_HM01. Then you store bit zero for TM01 and bit one for HM01. In a larger game, you want the item-to-machine mapping to be generated or centralized so the bag item order does not silently become the bit order. FireRed’s project structure uses many constant headers and data files, and the packet repeatedly points us to constants and data includes across modules, for example learn_move.c chunk_af3598df6f092723 includes constants/moves.h, while pokemon_summary_screen.c chunk_e8e17a42bbf17d46 includes constants/items.h and constants/moves.h. That is a sign of a C codebase using shared numeric identities carefully. The danger is not constants. The danger is assuming two constant spaces are the same because their names look similar.

A practical guardrail is to make conversion explicit. ItemIdToMachineIndex(itemId) should return either a machine index and move id, or failure if the item is not a TM/HM. Do not let random code subtract ITEM_TM01 and hope it remains correct forever, unless that arithmetic is the deliberately centralized implementation and all item ordering invariants are documented. In a lab, I will accept simple arithmetic if your item constants are contiguous and your comment says so. In a production-style architecture, I prefer a table because it survives reordering and supports exceptions.

Let me show the failure mode. Suppose today TM01 is item id 100, TM02 is 101, and HM01 is 150. A student writes machineIndex equals itemId minus 100. TM01 maps to zero and TM02 maps to one, fine. HM01 maps to fifty. The bitset has only, say, eight machine bits. Now HM01 either reads outside the field, always fails, or accidentally checks some unrelated bit. Worse, if the language does not protect you, you may mask a nonsense value and produce a plausible answer. That is the bug that “looks harmless and then infects every test,” as I said before. The correction is not complicated: centralize item-to-machine mapping.

Now we can tie this back to the species table continuation in pokefirered/src/data/pokemon/species_info.h chunk_2e759c52d0f4129a. Species records are broad. They are not just base stats. They include many facts that other systems query: growth, abilities, egg groups, and related identity-level properties depending on the exact generation and project. TM/HM learnsets are another species-level property, even if physically stored in a separate table for readability or layout. The conceptual pattern is the same as base stat calculation from the last lecture: runtime asks the species data for durable inputs, combines them with individual state, and writes or displays derived results elsewhere.

So, quick recap before we move into the reference signatures. TM/HM compatibility is a species capability question compressed as bits. Machine item identity, machine index, and move id are separate roles. Level-up learnsets answer a different question, because they need level and order. Move acquisition is a flow: resolve, validate, check current state, confirm, mutate. Battle reads the resulting move slots and PP; it should not re-own learnability. Evolution changes the species row used for future compatibility, but it does not rewrite move history by magic.

For the next board pass, I’m going to write the exact data structs I would expect in the lab: a species info record with base stats and a learnset pointer, a machine compatibility integer or array, a move table with PP, a party creature with four move ids and four PP values, and three resolver functions. Keep the three columns in your head while we do it: durable species facts, persistent individual state, and temporary UI flow. That separation is the difference between a creature RPG that stays understandable at three species and one that can scale to hundreds without every new move teaching rule becoming a cross-module negotiation.

## Source References
- pokefirered/src/daycare.c
- pokefirered/src/learn_move.c
- pokemon-showdown/data/mods/gen2stadium2/scripts.ts
- pokefirered/src/pokemon_summary_screen.c
- pokefirered/src/battle_setup.c
- pokefirered/src/battle_util.c
