Skip to content

Pokémon Browser Game: Start the game from trainer setup

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.

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 state

The order matters. Invalid input must stop before the state update. The renderer must read the updated state rather than read the form again.

  • New: game.js, the trainer form, fixed starter definitions, the initial state object, a stable starter lookup, a changing starter record, the starting coordinate, and the first game renderer.
  • Reused: The existing game.html page 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:

  1. Write JavaScript for a web page introduces the external script, values, variables, functions, conditions, and Console checks.
  2. Respond to user input with DOM events introduces DOM selection, form submission, input validation, status updates, and focus management.
  3. Store and render interface state introduces arrays, objects, deliberate null values, 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.

Complete this article when:

  • game.html keeps the existing site header, navigation, page heading, and footer;
  • the placeholder inside main is replaced by a labeled trainer-name field, a labeled native starter select, 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.html loads game.js with defer;
  • game.js uses 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 textContent and 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.

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:

  1. Which section introduces the game page?
  2. Which section owns the form and its instructions?
  3. Which section owns values produced by JavaScript?
  4. Which visible text must still make sense before JavaScript runs?
  5. 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.

  1. Select the Trainer name label. Focus must move to the text field.
  2. Select the Starter Pokémon label. Focus must move to the native select.
  3. Use only the keyboard to move through the field, select, and button in source order.
  4. Confirm that the summary shows the complete before-setup result.
  5. Confirm that the shared navigation still identifies the Game page as current.

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.

Trainer setup page before a game starts, with an empty fictional-name field, a starter select, and a current-game summary that reports the before-setup state.
Before setup, the form and every initial game value remain understandable even if JavaScript has not changed the page.
Trainer setup page before a game starts, with an empty fictional-name field, a starter select, and a current-game summary that reports the before-setup state.

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.

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 the browser cannot find game.js, compare the script src, file name, letter case, and project location.
  • If the message appears twice, inspect game.html for a duplicate script element.
  • If pokedex.js runs on the Game page, replace that script reference with game.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.

  • Why is maximum hit points part of a fixed starter definition?
  • Why will current hit points belong to a different object?
  • Why is caughtPokemon an empty array while activeEncounter is null?
  • 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:

  • statusMessage renders in the status paragraph; and
  • the complete caughtPokemon array 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.

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.

  1. Temporarily change the selector for trainer-form so it cannot match.
  2. Save and reload.
  3. Confirm that the Console shows the required error.
  4. Restore the correct selector.
  5. 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.

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:

  1. Cancel the form’s default navigation.
  2. Clear the previous aria-invalid attributes and validation messages.
  3. Read and trim the trainer name.
  4. Read the selected starter ID.
  5. If the name is empty, mark and focus the name field, show Enter a fictional trainer name., and return.
  6. If the starter ID is empty, mark and focus the select, show Choose a starter Pokémon., and return.
  7. 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.

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 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:

  1. visit each record in starterDefinitions with for...of;
  2. compare the record’s stable id with the selected ID;
  3. return the matching record immediately; and
  4. return null after 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.starter object that copies id, name, type, and maximumHitPoints from the fixed definition;
  • add currentHitPoints to the changing record and start it at the maximum;
  • create a new state.mapPosition object from the fixed starting coordinate;
  • set activeEncounter to null;
  • replace caughtPokemon with 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.

Submit each real choice and inspect the returned definition. Then test the unknown-value branch deliberately:

  1. Change the Snivy option value from snivy to snivy-test.
  2. Reload, choose Snivy, and submit a valid name.
  3. Confirm Choose a listed starter Pokémon., select focus, and no property error.
  4. Restore the option value to snivy.
  5. Reload and confirm that Snivy matches again.

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.

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:

  1. once after a valid submission has updated every state property; and
  2. once during initialization, after the event listener is registered.

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.

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-introduction replaces the old .placeholder-page role for the page heading and introduction;
  • .trainer-setup groups the form heading, instructions, and controls;
  • .trainer-form arranges the field groups and submit button in one clear sequence;
  • .form-field keeps each label, help text, control, and error close together;
  • .validation-message reserves a stable error area and makes the message identifiable through wording and placement, not color alone;
  • .game-summary groups the values produced by the renderer; and
  • .game-facts keeps 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:

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 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.

Completed trainer setup page after Nova chooses Snivy, with 40 of 40 hit points, the Pokémon Center position, no active encounter, no caught Pokémon, and a ready-to-explore status.
A valid submission creates one complete initial game state, and the summary renders every visible value from that state.
Completed trainer setup page after Nova chooses Snivy, with 40 of 40 hit points, the Pokémon Center position, no active encounter, no caught Pokémon, and a ready-to-explore status.

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:

game.html — partial main structure
<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:

game.js — partial program structure
"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:

game.html — complete main content
<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:

game.html — deferred game script
<script src="game.js" defer></script>

Use this complete game.js reference:

game.js — complete trainer setup 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:

styles.css — trainer setup additions
.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);
}

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:

  1. Validate game.html with the Level 1 HTML process.
  2. Review the CSS diagnostics in VS Code.
  3. Reload with the Console open and repeat every form route.
  4. Confirm that no temporary Console message or deliberate test value remains.
  5. Run git diff --check and inspect the complete diff.

Self-check

Complete these checks against the required result.

  1. Confirm that game.html keeps the shared page shell and loads one game.js file with defer.
  2. Confirm that every label, help text, error output, summary output, and status output has the documented relationship and initial text.
  3. Submit empty and whitespace-only names and confirm that no state summary changes.
  4. Submit a valid name with no starter and confirm the specific starter error and select focus.
  5. Submit each listed starter and confirm that its stable ID finds the correct fixed definition.
  6. Confirm that a valid setup creates a separate changing starter record with 40 of 40 hit points.
  7. Confirm that row 2, column 2 in state renders as Pokémon Center — row 3, column 3.
  8. Confirm that activeEncounter is null, caughtPokemon is an empty array, and the visible encounter and count agree.
  9. Submit HTML-looking and long fictional names and confirm that text remains literal, readable, and contained.
  10. Use only the keyboard for every form route and confirm visible, useful focus after each invalid result.
  11. Reload after a valid game and confirm that the complete before-setup state returns.
  12. Confirm that game.html passes HTML validation, styles.css has no unresolved author diagnostic, and every interaction leaves the Console free of red errors.
  13. Confirm that Home, Pokédex, and the Pokédex filter still pass their focused regression checks.

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.