Pokémon Browser Game: Start the game from trainer setup
Outcome
Section titled “Outcome”Replace the game-page placeholder with a trainer form and a current-game summary. A visitor will enter a fictional trainer name, choose Snivy, Fuecoco, or Totodile, and start a new game without reloading the page.
You will plan the HTML contract, fixed game definitions, changing state, validation route, and renderer before you write each part. The main route gives you the required behavior and tests. Optional assistance provides orientation, targeted hints, pseudocode, a partial structure, and a complete reference solution.
What you will practice
- Connect game.html to one deferred external JavaScript file.
- Design a labeled trainer form with separate name and starter validation results.
- Represent fixed starter definitions and changing game state with arrays, objects, and deliberate absence values.
- Match one selected option value to one starter definition by stable identity.
- Update game state only after every setup value is valid.
- Design one render function for trainer, starter, hit points, position, encounter, collection, and status values.
- Keep fictional trainer input as text and return focus to the first field that needs correction.
Why this stage comes before the map
Section titled “Why this stage comes before the map”The map, movement, encounters, and battles all need a current trainer and starter. This stage creates that starting state before later events try to change it.
Plan the program around this order:
submit event -> read and validate form values -> update game state -> render the current stateThe order matters. Invalid input must stop before the state update. The renderer must read the updated state rather than read the form again.
What is new and what is reused
Section titled “What is new and what is reused”- New:
game.js, the trainer form, fixed starter definitions, the initialstateobject, a stable starter lookup, a changing starter record, the starting coordinate, and the first game renderer. - Reused: The existing
game.htmlpage shell, shared navigation,styles.css, form labels, native controls, deferred scripts,querySelector, missing-element guards,submit,preventDefault,.trim(), conditions,textContent,focus(),aria-invalid,role="status", arrays, objects,for...of, and Console checks.
Complete these Module 2 lessons before you begin:
- Write JavaScript for a web page introduces the external script, values, variables, functions, conditions, and Console checks.
- Respond to user input with DOM events introduces DOM selection, form submission, input validation, status updates, and focus management.
- Store and render interface state introduces arrays, objects, deliberate
nullvalues, stable record lookup, fixed definitions, changing runtime records, and state-driven rendering.
Keep the three lesson tabs available. This article links to the relevant section when a Level 2 pattern first enters the game.
Starting point
Before you start
- The landing-page refinement, static Pokédex, and Pokédex filter pass their complete self-checks.
- game.html still contains the working game-page shell and placeholder main content from the first landing-page article.
- index.html, game.html, and pokedex.html still share working navigation and styles.css.
- pokedex.html loads pokedex.js, while game.html does not yet load a game script.
- You completed the three linked Module 2 web-interaction lessons.
- The repository contains the verified Pokédex-filter checkpoint and has no unexplained changes.
- Current state
- The website and Pokédex work. The game page identifies its future purpose, but it has no trainer form, game state, or game.js file.
- First action
- Open game.html. Before you edit it, write down the three jobs that the introduction, trainer form, and current-game summary must perform.
- First checkpoint
- The static trainer form and current-game summary have connected labels, useful initial text, and a logical keyboard order before JavaScript changes them.
- Help trigger
- Open the nearest assistance level or ask for help if you cannot name the next subgoal, a label does not focus its control, game.js does not load, a selector returns null, invalid input changes game state, one starter choice finds the wrong definition, HTML-looking trainer text creates an element, or the visible summary disagrees with state.
Required result
Section titled “Required result”Complete this article when:
game.htmlkeeps the existing site header, navigation, page heading, and footer;- the placeholder inside
mainis replaced by a labeled trainer-name field, a labeled native starterselect, a submit button, connected help and error text, and a current-game summary; - the starter control offers Snivy, Fuecoco, and Totodile through stable lowercase values;
game.htmlloadsgame.jswithdefer;game.jsuses strict mode and stops with a specific Console error when a required interface element is missing;- fixed starter definitions remain separate from the changing starter record in game state;
- one state object contains the trainer name, starter, map position, active encounter, caught collection, and status message;
- an empty or whitespace-only name shows a specific error, applies
aria-invalid="true", returns focus to the name field, and changes no game state; - a missing or unknown starter value shows a specific error, applies
aria-invalid="true", returns focus to the starter control, and changes no game state; - a valid submission updates every initial state property before one render function updates the summary;
- the visible result identifies the trainer, starter, current and maximum hit points, Pokémon Center position, absence of an encounter, caught total, and next status;
- fictional trainer input enters the page through
textContentand cannot create HTML elements; - a reload restores the documented before-setup state because this version stores state only in current page memory;
- no map cells, player marker, movement behavior, encounter chance, battle behavior, persistence, API, or framework is added; and
- the page passes HTML, CSS, JavaScript, keyboard, narrow-width, zoom, and Console checks.
1. Plan and build the static interface
Section titled “1. Plan and build the static interface”Keep the existing document metadata, shared site header, navigation, main element, and footer in game.html. Replace only the placeholder content inside main.
Before writing markup, answer these questions in a project note or HTML comments:
- Which section introduces the game page?
- Which section owns the form and its instructions?
- Which section owns values produced by JavaScript?
- Which visible text must still make sense before JavaScript runs?
- Which relationships need stable IDs?
Use this interface contract. It defines the required relationships without choosing the complete markup for you.
| Region | Required content | Stable hooks and relationships |
|---|---|---|
| Introduction | Game page, Pokémon Trail game, and a short explanation of trainer setup | Class game-introduction |
| Trainer setup | Heading Set up your trainer, one instruction, and one form | Heading ID trainer-setup-heading; section labelled by that heading; form ID trainer-form and class trainer-form |
| Trainer name field | Visible label, help text, text input, and a separate empty validation output | Control ID trainer-name; help ID trainer-name-help; error ID trainer-name-error; one aria-describedby relationship to both text elements |
| Starter field | Visible label, help text, native select, and a separate empty validation output | Control ID starter-choice; help ID starter-help; error ID starter-error; one aria-describedby relationship to both text elements |
| Starter choices | One instruction option followed by Snivy, Fuecoco, and Totodile | Empty instruction value; stable values snivy, fuecoco, and totodile |
| Submit action | Button text Start game | Native submit button |
| Current game | Heading, one fact list, and one status paragraph | Heading ID game-summary-heading; section labelled by that heading; class game-summary |
Give the summary these stable outputs and initial meanings:
| Output ID | Initial visible text |
|---|---|
trainer-output |
No trainer yet. |
starter-output |
No starter selected. |
hit-points-output |
Not available. |
position-output |
Not set. |
encounter-output |
No active encounter. |
caught-output |
0 |
game-status |
Enter a fictional trainer name and choose a starter. |
Add role="status" to each validation output and to game-status. Keep the validation outputs separate so each control can report its own result.
Read Add an interface for the status program before you choose the form elements. Read Read one selected option before you choose the option values.
Save and reload game.html before adding JavaScript.
- Select the Trainer name label. Focus must move to the text field.
- Select the Starter Pokémon label. Focus must move to the native select.
- Use only the keyboard to move through the field, select, and button in source order.
- Confirm that the summary shows the complete before-setup result.
- Confirm that the shared navigation still identifies the Game page as current.
If it does not work
Section titled “If it does not work”If a label does not move focus, compare its for value with the control’s id. If the navigation or footer changes, restore the last passing game.html and replace only the placeholder content inside main.
Assistance 1 — Locate the three interface regions
Keep the page shell outside main. Inside main, create one section for the introduction, one for input, and one for output. Work on one region at a time and reload after each region.
Assistance 2 — Map each form relationship before writing attributes
For each field, write four names beside each other: label target, control ID, help ID, and error ID. The label’s for must match the control ID. The control’s aria-describedby must contain the help and error IDs. The two validation outputs need different IDs.
Assistance 3 — Build the main content in three passes
First create the three sections and their headings. Next complete the form with both labeled controls, help text, error outputs, options, and submit button. Last add the current-game list and status paragraph. Run the label and keyboard checks after the second pass, then compare every initial output after the third pass.

Checkpoint: The game page has a stable HTML contract
- What now works
- The trainer form and current-game summary are understandable without JavaScript, every label is connected, the keyboard order follows the source, and the shared page shell remains intact.
- Files changed
game.html- What remains
- Connect one deferred game.js file and model the game's fixed definitions and changing state.
- Next action
- Create game.js beside the other project files and connect it from the head of game.html.
- If it does not work
- Restore the shared shell from the last passing commit, then add one section at a time and repeat the label and keyboard checks.
2. Connect game.js
Section titled “2. Connect game.js”Create game.js beside game.html. Start the file with strict mode and one temporary Console message that proves the file ran.
In the head of game.html, load game.js after the shared stylesheet. Use defer so the browser finishes parsing the HTML before the script selects the trainer interface.
Reuse Connect JavaScript to the page if you need the exact external-script pattern.
Open the Console and reload game.html. Confirm that the Console shows your connection message once and no red error.
Submitting the form still reloads the page at this point. The connection works, but the script does not handle the event yet. Remove the temporary message after this check passes.
If it does not work
Section titled “If it does not work”- If the browser cannot find
game.js, compare the scriptsrc, file name, letter case, and project location. - If the message appears twice, inspect
game.htmlfor a duplicate script element. - If
pokedex.jsruns on the Game page, replace that script reference withgame.js. Each page keeps its own behavior file.
3. Model fixed definitions and changing state
Section titled “3. Model fixed definitions and changing state”Before writing the objects, separate facts that define the game from values that can change during one game.
Add one starterDefinitions array. Each record needs id, name, type, and maximumHitPoints properties.
| ID | Name | Type | Maximum hit points |
|---|---|---|---|
snivy |
Snivy | Grass | 40 |
fuecoco |
Fuecoco | Fire | 40 |
totodile |
Totodile | Water | 40 |
Add one startingPosition object with row 2 and column 2. This object defines the shared start coordinate. It is not the player’s current position.
Then create one state object for the current game:
| Property | Initial value | Meaning before setup |
|---|---|---|
trainerName |
Empty string | No required trainer-name string exists yet |
starter |
null |
No changing starter record is active |
mapPosition |
null |
No current map position is active |
activeEncounter |
null |
No wild Pokémon encounter is active |
caughtPokemon |
Empty array | The caught collection exists and contains no records |
statusMessage |
The same instruction shown in game-status |
The interface can state the next required action |
Use one meaning for each absence value. Do not switch the starter between null, an empty string, and an empty object.
Read Represent the interface state for the object, property, and array model. Read Represent a deliberate absence value for the null values. Read why a const binding can contain changing properties before the submit handler changes state.
Think before you write
Section titled “Think before you write”- Why is maximum hit points part of a fixed starter definition?
- Why will current hit points belong to a different object?
- Why is
caughtPokemonan empty array whileactiveEncounterisnull? - Which values should return after a page reload?
Save and reload. The Console must remain free of errors. Compare every state property with one visible summary row and identify the two exceptions:
statusMessagerenders in the status paragraph; and- the complete
caughtPokemonarray will render as entries in a later article, while this article shows only its derived length.
Assistance 1 — Separate game rules from the current game
Put reusable species facts and the shared start coordinate before state. Put values that describe one active game inside state. A battle will later change the current starter’s hit points, but it must not rewrite Snivy’s definition.
Assistance 2 — Choose absence values from their meaning
Use an empty string when a required string has not been entered, null when no current record or position exists, and an empty array when a collection exists but has no members. Keep those meanings stable in validation and rendering.
4. Select and guard the stable interface
Section titled “4. Select and guard the stable interface”Use querySelector to select the form, both controls, both validation outputs, all six fact outputs, and the status output. Store each result in a named const whose name describes the element’s role.
Before any function uses those elements, add one guard that checks every selection. If any selection is missing, stop the script with this exact error:
Required trainer setup elements are missing.Reuse Connect HTML to JavaScript through the DOM for the relationship between each HTML id, selector string, and selected DOM element.
Test the guard deliberately
Section titled “Test the guard deliberately”- Temporarily change the selector for
trainer-formso it cannot match. - Save and reload.
- Confirm that the Console shows the required error.
- Restore the correct selector.
- Reload and confirm that the error disappears.
Do not continue while the deliberate test value remains in the file.
Assistance 1 — Create a selector checklist from the HTML contract
Start with the seven summary and status IDs, then add the form, two controls, and two error outputs. Mark an ID only after its selector returns an element in the Console.
Assistance 2 — Compare one selector with one ID at a time
A selector string starts with #. The matching HTML id does not. If the guard throws, inspect the first selected variable that contains null, then compare only that selector with the HTML.
Assistance 3 — Write the guard as one complete contract
Place all DOM selections together. After them, write one condition joined with logical OR operators. Negate each selected element so the condition becomes true when at least one element is missing. Throw the required Error inside that condition.
Checkpoint: The game data and DOM contract are explicit
- What now works
- Fixed starter definitions, one before-setup state object, twelve stable DOM selections, and one missing-elements guard load without a Console error after the deliberate guard test is restored.
- Files changed
game.html, game.js- What remains
- Handle form submission and reject invalid values before any state property changes.
- Next action
- Name the submit handler, then write the validation order before you write its JavaScript statements.
- If it does not work
- Return game.js to the fixed definitions, state, selections, and guard. Correct one selector at a time until a reload produces no error.
5. Read and validate both setup values
Section titled “5. Read and validate both setup values”Write one named submit handler and register it on the form’s submit event. Use the form event so both the button and Enter follow the same path.
Plan the handler in this order:
- Cancel the form’s default navigation.
- Clear the previous
aria-invalidattributes and validation messages. - Read and trim the trainer name.
- Read the selected starter ID.
- If the name is empty, mark and focus the name field, show Enter a fictional trainer name., and return.
- If the starter ID is empty, mark and focus the select, show Choose a starter Pokémon., and return.
- Temporarily log the two valid values so you can inspect them before state changes exist.
No state property may change in this version of the handler.
Use Listen for the submit event and Read, validate, and show the input for the event, validation, status, and focus patterns.
Check every validation route
Section titled “Check every validation route”| Form state | Expected result |
|---|---|
| Both values missing | Name error; name field has aria-invalid="true" and focus |
| Name contains only spaces | Same result as an empty name |
| Name present, starter missing | Starter error; select has aria-invalid="true" and focus |
| Name and starter present | One Console line with the trimmed name and stable starter ID |
Use both the submit button and Enter from the name field. Remove the temporary Console call only after every real starter option produces its matching lowercase ID.
If it does not work
Section titled “If it does not work”- If the page reloads, inspect the first statement in the handler.
- If spaces pass validation, inspect the value that the empty-string condition tests.
- If an error appears but focus does not move, inspect the invalid branch before its
return. - If the handler runs during page load, inspect the function value passed to
addEventListener.
Assistance 1 — Identify the first state that must stop submission
Trace the both-missing case first. The trimmed name is empty, so the name branch must own the message and focus. The return statement must stop the starter check and all later work during that submission.
Assistance 2 — Connect each invalid result to one control
The name branch changes only the name field and name error. The starter branch changes only the select and starter error. Clear both previous results near the start so a corrected field does not keep an old error.
Assistance 3 — Write the submit handler as four subgoals
Use this pseudocode: cancel navigation; clear old validation; read both current values; handle the name failure and return; handle the starter failure and return; expose the two valid values. Test each branch before adding starter lookup or state updates.
6. Match one definition and create the initial game
Section titled “6. Match one definition and create the initial game”Write findStarterDefinition(starterId) before the submit handler. The function must:
- visit each record in
starterDefinitionswithfor...of; - compare the record’s stable
idwith the selected ID; - return the matching record immediately; and
- return
nullafter the loop if no record matches.
Read Find one record by a stable identity before you use the lookup result.
In the valid form path, call the function. If it returns null, mark and focus the starter select, show Choose a listed starter Pokémon., and return before state changes.
When the lookup succeeds, update the complete initial state:
- use the trimmed form value for
state.trainerName; - create a new
state.starterobject that copiesid,name,type, andmaximumHitPointsfrom the fixed definition; - add
currentHitPointsto the changing record and start it at the maximum; - create a new
state.mapPositionobject from the fixed starting coordinate; - set
activeEncountertonull; - replace
caughtPokemonwith an empty array; and - report that the trainer and starter are ready to explore in
statusMessage.
Do not assign the fixed definition object directly to state.starter. Future battle actions must be able to change current hit points without changing the definition.
Read Keep fixed definitions separate from changing records for this responsibility boundary.
Check known and unknown values
Section titled “Check known and unknown values”Submit each real choice and inspect the returned definition. Then test the unknown-value branch deliberately:
- Change the Snivy option value from
snivytosnivy-test. - Reload, choose Snivy, and submit a valid name.
- Confirm Choose a listed starter Pokémon., select focus, and no property error.
- Restore the option value to
snivy. - Reload and confirm that Snivy matches again.
Inspect the fixed and changing records
Section titled “Inspect the fixed and changing records”After a valid setup, temporarily change state.starter.currentHitPoints to 1. Log that value and the fixed definition’s maximum. The Console must show 1 for the changing record and 40 for the fixed definition.
Restore current hit points from the fixed maximum and remove the temporary statements before you continue.
Assistance 1 — Trace one stable identity from HTML to state
Choose Snivy. The select supplies snivy. The loop compares that string with each definition’s id. The matching definition supplies the display name, type, and maximum hit points used to create a different changing object.
Assistance 2 — Place every failure before the first state assignment
The empty-name check, empty-starter check, and unknown-starter check must all return before state.trainerName changes. Search the handler for the first state. assignment and confirm that all three guards appear above it.
Assistance 3 — Separate lookup, copy, and initialization
First find one fixed definition. Second create a new starter object from its properties. Third create a new position object. Last assign the deliberate absence, empty collection, and status values. Inspect the complete state before rendering anything.
7. Render the current game from state
Section titled “7. Render the current game from state”Write one renderGame() function. It must read state and update the corresponding DOM text. It must not read the form, choose a starter, or change game data.
Use this rendering contract:
| State condition or value | Visible result |
|---|---|
starter or mapPosition is null |
Keep the four before-setup trainer, starter, hit-point, and position messages |
| Complete trainer state | Trainer name; starter name and type; current and maximum hit points |
| Map row and column | Pokémon Center — row 3, column 3 after adding 1 for the visible values |
activeEncounter is null |
No active encounter. |
| Active encounter exists | Its name property |
caughtPokemon |
Its derived length, converted to text |
statusMessage |
The current status text |
Use textContent for every update. A fictional trainer name such as <strong>Nova</strong> must appear as literal text and must not create a strong element.
Read the render function phases for the same separation in the task tracker.
Call renderGame() in two places:
- once after a valid submission has updated every state property; and
- once during initialization, after the event listener is registered.
Check the two render states
Section titled “Check the two render states”First reload the page and compare every initial output with the before-setup state. Then start one valid game and compare every visible value with the current state object.
The form remains visible at this checkpoint. Submitting another valid name and starter creates a new initial game state and replaces the previous summary. Submitting an invalid form after a game has started reports the validation error but keeps the current game state unchanged.
Intermediate focus contract
Section titled “Intermediate focus contract”This checkpoint does not contain a movement control yet. After a valid submission, keep focus on the form control that initiated the submit and use the polite game-status region to report the result.
Do not add an enabled movement button that has no movement behavior. The later movement article will update the successful setup path so focus moves to the first enabled movement control after the map and movement rules exist.
Assistance 1 — Map one state property to one output
Start with state.trainerName and trainer-output. Then map starter, hit points, position, encounter, collection length, and status in the same way. Keep the form controls out of the renderer.
Assistance 2 — Protect property access with the absence checks
Do not read state.starter.name until the starter is not null. Do not read row or column until the position is not null. Render the initial messages in the absence branch and the current values in the complete-state branch.
Assistance 3 — Build the renderer in three phases
First render trainer, starter, hit points, and position from their shared complete-state check. Next render the encounter from its own null check. Last render the derived caught count and status message. Call the renderer before setup, then call it again only after a valid state update.
Checkpoint: A valid trainer creates one current game
- What now works
- Invalid values change no game state. A valid fictional name and listed starter create the complete initial state, preserve the fixed definition, and render matching trainer, starter, hit-point, position, encounter, collection, and status text.
- Files changed
game.html, game.js- What remains
- Fit the new interface into the existing visual system and run the complete trainer-setup verification route.
- Next action
- Open styles.css and identify which existing spacing, border, type, control, and focus rules the trainer form and summary can reuse.
- If it does not work
- Trace one submission in this order: form values, validation branches, lookup result, state properties, render call, and visible text. Stop at the first disagreement.
8. Fit the interface into the existing visual system
Section titled “8. Fit the interface into the existing visual system”Keep the form and summary in normal document flow. The overworld article will give CSS Grid a map-specific job. Trainer setup does not need a new page-layout system.
Write focused rules for these class roles:
.game-introductionreplaces the old.placeholder-pagerole for the page heading and introduction;.trainer-setupgroups the form heading, instructions, and controls;.trainer-formarranges the field groups and submit button in one clear sequence;.form-fieldkeeps each label, help text, control, and error close together;.validation-messagereserves a stable error area and makes the message identifiable through wording and placement, not color alone;.game-summarygroups the values produced by the renderer; and.game-factskeeps the summary list readable with long trainer and Pokémon names.
In the two existing grouped selectors that contain .placeholder-page, replace that class with .game-introduction. The game page no longer represents an unimplemented placeholder.
Reuse the existing custom properties and visual roles. Use normal flow or Flexbox for the form’s one-dimensional relationships. Reuse these Level 1 sections when needed:
- Group related content
- Identify the container and its items
- Add visible link interaction states
- Make the narrow layout the default
Buttons, inputs, and selects must use readable text, fit their containing block, and retain a visible keyboard-focus indicator. Long text must wrap instead of widening the page.
Test the Game page near 320px, at a wide viewport, and at 200% browser zoom. Use a long fictional trainer name. Confirm that:
- the field, select, and button stay inside the page;
- labels, help text, errors, and summary values do not overlap;
- the keyboard-focus indicator remains visible;
- the summary order remains the same; and
- the page has no unintended horizontal scrolling.
If it does not work
Section titled “If it does not work”If a control widens the page, inspect its width, padding, border, and containing block before changing the complete layout. If a long value does not wrap, inspect the item that owns that text for a fixed width or an unbroken test string.

Trainer setup assistance
Section titled “Trainer setup assistance”The checkpoint assistance above supports one decision or test at a time. Open one of the following levels when you need a larger implementation frame. Compare responsibilities and IDs before you copy any part into your project.
Assistance 4 — Use a partial trainer setup structure
Use this HTML outline when the relationships are clear but an empty editor is blocking the next action:
<section class="game-introduction"> <!-- Add the page label, h1, and introduction. --></section>
<section class="trainer-setup" aria-labelledby="trainer-setup-heading"> <h2 id="trainer-setup-heading">Set up your trainer</h2> <!-- Add one instruction and the trainer-form. --> <!-- Build the name field, starter field, and submit action. --></section>
<section class="game-summary" aria-labelledby="game-summary-heading"> <h2 id="game-summary-heading">Current game</h2> <!-- Add the seven documented outputs and their initial text. --></section>Use this JavaScript structure when you can explain each responsibility but need help ordering the file:
"use strict";
// Define the three fixed starter records and shared starting coordinate.const starterDefinitions = [];const startingPosition = {};
// Represent the current game before trainer setup.const state = { trainerName: "", starter: null, mapPosition: null, activeEncounter: null, caughtPokemon: [], statusMessage: "Enter a fictional trainer name and choose a starter.",};
// Select and guard the stable interface elements.
function findStarterDefinition(starterId) { // Return the matching fixed definition or null.}
function renderGame() { // Render the before-setup or complete trainer values. // Render the encounter, caught count, and status.}
function handleTrainerSubmit(event) { event.preventDefault();
// Clear old validation and read both current values. // Return from the empty-name, empty-starter, or unknown-starter path. // Create the complete initial state, then render it.}
trainerForm.addEventListener("submit", handleTrainerSubmit);renderGame();Complete one comment group at a time. Run the related checkpoint test before you move to the next group.
Assistance 5 — Compare with the complete trainer setup referenceExample solution
This reference matches the guided starting project. Use it to compare structure, repair one failed checkpoint, or reconstruct a working baseline. Read the explanation and repeat the tests after any copied change.
Replace the placeholder content inside main with this reference:
<section class="game-introduction"> <p class="section-label">Game page</p> <h1>Pokémon Trail game</h1> <p> Create a fictional trainer and choose the starter Pokémon that will travel with you. This setup creates the state that later game stages will use. </p></section>
<section class="trainer-setup" aria-labelledby="trainer-setup-heading"> <h2 id="trainer-setup-heading">Set up your trainer</h2> <p>Use a fictional trainer name and choose one starter.</p>
<form class="trainer-form" id="trainer-form"> <div class="form-field"> <label for="trainer-name">Trainer name</label> <p id="trainer-name-help">Enter a fictional name.</p> <input id="trainer-name" name="trainerName" type="text" aria-describedby="trainer-name-help trainer-name-error" > <p class="validation-message" id="trainer-name-error" role="status" ></p> </div>
<div class="form-field"> <label for="starter-choice">Starter Pokémon</label> <p id="starter-help">Choose one starter for this game.</p> <select id="starter-choice" name="starterChoice" aria-describedby="starter-help starter-error" > <option value="">Choose a starter</option> <option value="snivy">Snivy</option> <option value="fuecoco">Fuecoco</option> <option value="totodile">Totodile</option> </select> <p class="validation-message" id="starter-error" role="status" ></p> </div>
<button type="submit">Start game</button> </form></section>
<section class="game-summary" aria-labelledby="game-summary-heading"> <h2 id="game-summary-heading">Current game</h2> <ul class="game-facts"> <li>Trainer: <span id="trainer-output">No trainer yet.</span></li> <li>Starter: <span id="starter-output">No starter selected.</span></li> <li>Hit points: <span id="hit-points-output">Not available.</span></li> <li>Position: <span id="position-output">Not set.</span></li> <li>Encounter: <span id="encounter-output">No active encounter.</span></li> <li>Caught Pokémon: <span id="caught-output">0</span></li> </ul> <p id="game-status" role="status"> Enter a fictional trainer name and choose a starter. </p></section>Load the game script after the stylesheet in the document head:
<script src="game.js" defer></script>Use this complete game.js reference:
"use strict";
const starterDefinitions = [ { id: "snivy", name: "Snivy", type: "Grass", maximumHitPoints: 40, }, { id: "fuecoco", name: "Fuecoco", type: "Fire", maximumHitPoints: 40, }, { id: "totodile", name: "Totodile", type: "Water", maximumHitPoints: 40, },];
const startingPosition = { row: 2, column: 2,};
const state = { trainerName: "", starter: null, mapPosition: null, activeEncounter: null, caughtPokemon: [], statusMessage: "Enter a fictional trainer name and choose a starter.",};
const trainerForm = document.querySelector("#trainer-form");const trainerNameInput = document.querySelector("#trainer-name");const trainerNameError = document.querySelector("#trainer-name-error");const starterChoice = document.querySelector("#starter-choice");const starterError = document.querySelector("#starter-error");const trainerOutput = document.querySelector("#trainer-output");const starterOutput = document.querySelector("#starter-output");const hitPointsOutput = document.querySelector("#hit-points-output");const positionOutput = document.querySelector("#position-output");const encounterOutput = document.querySelector("#encounter-output");const caughtOutput = document.querySelector("#caught-output");const gameStatus = document.querySelector("#game-status");
if ( !trainerForm || !trainerNameInput || !trainerNameError || !starterChoice || !starterError || !trainerOutput || !starterOutput || !hitPointsOutput || !positionOutput || !encounterOutput || !caughtOutput || !gameStatus) { throw new Error("Required trainer setup elements are missing.");}
function findStarterDefinition(starterId) { for (const starterDefinition of starterDefinitions) { if (starterDefinition.id === starterId) { return starterDefinition; } }
return null;}
function renderGame() { if (state.starter === null || state.mapPosition === null) { trainerOutput.textContent = "No trainer yet."; starterOutput.textContent = "No starter selected."; hitPointsOutput.textContent = "Not available."; positionOutput.textContent = "Not set."; } else { trainerOutput.textContent = state.trainerName; starterOutput.textContent = `${state.starter.name} — ${state.starter.type}`; hitPointsOutput.textContent = `${state.starter.currentHitPoints} of ${state.starter.maximumHitPoints}`;
const visibleRow = state.mapPosition.row + 1; const visibleColumn = state.mapPosition.column + 1;
positionOutput.textContent = `Pokémon Center — row ${visibleRow}, column ${visibleColumn}`; }
if (state.activeEncounter === null) { encounterOutput.textContent = "No active encounter."; } else { encounterOutput.textContent = state.activeEncounter.name; }
caughtOutput.textContent = `${state.caughtPokemon.length}`; gameStatus.textContent = state.statusMessage;}
function handleTrainerSubmit(event) { event.preventDefault();
trainerNameInput.removeAttribute("aria-invalid"); starterChoice.removeAttribute("aria-invalid"); trainerNameError.textContent = ""; starterError.textContent = "";
const trainerName = trainerNameInput.value.trim(); const starterId = starterChoice.value;
if (trainerName === "") { trainerNameInput.setAttribute("aria-invalid", "true"); trainerNameError.textContent = "Enter a fictional trainer name."; trainerNameInput.focus(); return; }
if (starterId === "") { starterChoice.setAttribute("aria-invalid", "true"); starterError.textContent = "Choose a starter Pokémon."; starterChoice.focus(); return; }
const starterDefinition = findStarterDefinition(starterId);
if (starterDefinition === null) { starterChoice.setAttribute("aria-invalid", "true"); starterError.textContent = "Choose a listed starter Pokémon."; starterChoice.focus(); return; }
state.trainerName = trainerName; state.starter = { id: starterDefinition.id, name: starterDefinition.name, type: starterDefinition.type, currentHitPoints: starterDefinition.maximumHitPoints, maximumHitPoints: starterDefinition.maximumHitPoints, }; state.mapPosition = { row: startingPosition.row, column: startingPosition.column, }; state.activeEncounter = null; state.caughtPokemon = []; state.statusMessage = `${trainerName} and ${starterDefinition.name} are ready to explore.`;
renderGame();}
trainerForm.addEventListener("submit", handleTrainerSubmit);
renderGame();Replace .placeholder-page with .game-introduction in the two existing grouped selectors. Then add these focused rules:
.trainer-setup,.game-summary { margin-block-start: var(--space-lg); border: 2px solid var(--color-brand); border-radius: var(--radius-card); background-color: var(--color-surface); padding: var(--space-lg);}
.trainer-setup > p { margin-block: 0 var(--space-lg); color: var(--color-muted);}
.trainer-form,.form-field { display: flex; flex-direction: column;}
.trainer-form { gap: var(--space-lg);}
.form-field { gap: var(--space-xs);}
.form-field label { font-weight: bold;}
.form-field p { margin: 0;}
.form-field input,.form-field select,.trainer-form button { inline-size: 100%; max-inline-size: 30rem; min-block-size: 2.75rem; border: 2px solid var(--color-brand); border-radius: var(--radius-small); color: var(--color-text); background-color: var(--color-surface); padding-block: var(--space-xs); padding-inline: var(--space-sm); font: inherit;}
.trainer-form button { color: var(--color-surface); background-color: var(--color-accent); font-weight: bold;}
.form-field input:focus-visible,.form-field select:focus-visible,.trainer-form button:focus-visible { outline: 3px solid var(--color-focus); outline-offset: 3px;}
.form-field [aria-invalid="true"] { border-color: var(--color-accent);}
.validation-message { min-block-size: 1.6em; color: var(--color-accent); font-weight: bold;}
.game-facts { display: flex; flex-direction: column; gap: var(--space-xs); margin: 0; padding-inline-start: var(--space-lg);}
.game-facts li { overflow-wrap: anywhere;}
#game-status { margin-block: var(--space-lg) 0; border-left: 0.25rem solid var(--color-accent); background-color: var(--color-page); padding: var(--space-md);}9. Verify the complete trainer route
Section titled “9. Verify the complete trainer route”Start each independent test from a fresh reload unless the row states another precondition.
| Test | Action | Expected result |
|---|---|---|
| Initial state | Reload game.html |
Before-setup summary appears; no Console error |
| Missing both | Submit both instruction values | Name error and name focus; state summary stays unchanged |
| Whitespace name | Enter spaces and choose a starter | Same name error; no trainer enters the summary |
| Missing starter | Enter Nova and keep the first option |
Starter error and select focus; state summary stays unchanged |
| Each starter | Start a fresh game with each listed starter | Matching name, type, and 40 of 40 appear |
| Invalid after valid | Start one valid game, then submit an empty name | Name error appears; the current trainer and starter stay unchanged |
| Valid after valid | Start one valid game, then submit a different valid name and starter | One complete new initial state replaces the previous game summary |
| Safe text | Submit <strong>Nova</strong> with a starter |
Literal angle-bracket text appears; no strong element is created |
| Long text | Submit a long fictional name near 320px and at 200% zoom |
Complete text remains readable and contained |
| Keyboard | Complete setup without a pointer | Native controls and submit route work; focus stays visible |
| Reload boundary | Start a game, then reload | Before-setup state returns; no value persists |
| Regression | Open Home and Pokédex, then use the filter | Existing navigation, catalog, and filtering still work |
For the focus test, enter document.activeElement in the Console after each invalid result. Compare the DOM result with the visible focus indicator. Use Test keyboard operation and focus for the Module 2 inspection method.
Then complete the source checks:
- Validate
game.htmlwith the Level 1 HTML process. - Review the CSS diagnostics in VS Code.
- Reload with the Console open and repeat every form route.
- Confirm that no temporary Console message or deliberate test value remains.
- Run
git diff --checkand inspect the complete diff.
Self-check
Complete these checks against the required result.
- Confirm that game.html keeps the shared page shell and loads one game.js file with defer.
- Confirm that every label, help text, error output, summary output, and status output has the documented relationship and initial text.
- Submit empty and whitespace-only names and confirm that no state summary changes.
- Submit a valid name with no starter and confirm the specific starter error and select focus.
- Submit each listed starter and confirm that its stable ID finds the correct fixed definition.
- Confirm that a valid setup creates a separate changing starter record with 40 of 40 hit points.
- Confirm that row 2, column 2 in state renders as Pokémon Center — row 3, column 3.
- Confirm that activeEncounter is null, caughtPokemon is an empty array, and the visible encounter and count agree.
- Submit HTML-looking and long fictional names and confirm that text remains literal, readable, and contained.
- Use only the keyboard for every form route and confirm visible, useful focus after each invalid result.
- Reload after a valid game and confirm that the complete before-setup state returns.
- Confirm that game.html passes HTML validation, styles.css has no unresolved author diagnostic, and every interaction leaves the Console free of red errors.
- Confirm that Home, Pokédex, and the Pokédex filter still pass their focused regression checks.
Next step or safe stopping point
Section titled “Next step or safe stopping point”Record a Git checkpoint after the complete self-check passes. The stable state is:
- the trainer form rejects invalid values before state changes;
- each valid choice finds one fixed starter definition;
- one state object owns the current trainer, starter, position, encounter, collection, and status;
- one render function makes the summary match that state; and
- a reload restores the before-setup result.
Do not add movement controls or map cells at this checkpoint. Continue to Render the overworld map when that article is available. The next first action is to define the fixed 5 × 5 map data without changing the working trainer form.