Pokémon Browser Game: Enter a wild encounter
Outcome
Section titled “Outcome”An accepted move into tall grass can start one wild encounter. The game selects from at least three fixed species, stores a separate record with the wild Pokémon’s current hit points, and shows an encounter view. The selected Pokémon stays the same when the page renders again. Movement is unavailable until the game starts again. Battle actions come in the next article.
What you will practice
- Use one percentage check for each accepted move into tall grass.
- Select one valid definition from a fixed array only when that check succeeds.
- Copy the selected definition into changing encounter state before rendering.
- Show either the exploration region or encounter region from the active-encounter state.
- Make random outcomes repeatable during development, then restore normal play.
- Move focus to the shown encounter heading when movement becomes unavailable.
Why this stage matters
Section titled “Why this stage matters”Movement already checks the destination before it changes state.mapPosition. That accepted destination is the one place to decide whether grass starts an encounter. A render function may run several times, so it must display the stored result without rolling again.
accepted grass move → chance decision → no encounter OR fixed species → changing encounter state → render → focusA failed chance keeps the trainer on the new grass square. A successful chance keeps that coordinate too, but changes which game section is visible.
What is new and what is reused
Section titled “What is new and what is reused”- New here: A fixed encounter list, a documented percentage, a separate chance and species selection, a changing wild-Pokémon record, an encounter view, and a source-code setting for repeatable tests.
- Reused from Module 2: The bounded random-result pattern explains
Math.random(), the percentage boundary, andMath.floor()with an array. The controlled-test pattern shows how one development setting replaces an unpredictable result. Interface state and rendering keep fixed definitions separate from changing values. Earlier project articles used state and rendering; read these sections before adding randomness. - Reused from this build:
moveTrainer(),findMapTile(),renderGame(),state.activeEncounter, and the movement buttons. The Pokédex filter article used HTMLhiddenand the matching JavaScript property. The DOM events focus pattern explains how to focus a heading after the visible section changes.
Starting point
Before you start
- The Move one step article passes its self-check and has a passing Git checkpoint.
- game.html has a trainer form, a 25-tile map, four movement buttons, and a current-game summary.
- game.js stores mapPosition and activeEncounter in state. The active encounter starts as null.
- The Pokédex and assets folder provide local Pokémon names, numbers, and images.
- Current state
- Grass movement changes the trainer position but never creates an encounter. The summary says No active encounter.
- First action
- In game.js, plan at least three allowed wild species and write down the encounter percentage and valid array indexes before adding random code.
- First checkpoint
- The fixed encounter list and percentage are in game.js; moving through the current map still works exactly as before.
- Help trigger
- Open the nearest Assistance level or ask for help if a grass move rolls more than once, a rerender changes the wild Pokémon, the known test cannot select its species, movement remains active during an encounter, or focus disappears.
Required result
Section titled “Required result”Complete this stage when:
- the fixed encounter list has at least three species already represented in the project’s Pokédex, each with a unique Pokédex number, name, type, local image path, and starting hit points;
- the chosen encounter percentage is a documented whole number from
1through99and stays unchanged during tests; - only an accepted move into grass makes one chance check; path, Center, blocked, and outside-map results make none;
- a failed check keeps the new grass position, reports no encounter, and leaves exploration available;
- a successful check uses a second random value to select one valid list index, creates a new changing record, and assigns it to
state.activeEncounterbefore rendering; - the record keeps the selected number, name, type, image, current hit points, and maximum hit points; changing its hit points later cannot change the fixed definition;
- the encounter view shows the wild image, identity, wild hit points, starter, and starter hit points while the exploration region is hidden and movement is unavailable;
- the encounter view links directly to the trainer setup field and tells the player that submitting the form again returns to the map;
- the encounter heading receives visible focus after an encounter starts; a new valid trainer setup returns focus to North and clears the encounter;
- rerendering, changing viewport size, or updating unrelated text never makes another encounter decision;
- controlled no-encounter and known-encounter modes reach their results through the same state-update and render route as normal mode, and the saved checkpoint uses normal mode; and
- the page remains readable near 320 CSS pixels and at 200% browser zoom, with no horizontal page scrolling.
Do not add Attack, Throw Poké Ball, Run, capture, wild response, or fainting logic here. Restarting trainer setup is the available way to leave this stage’s encounter view.
1. Plan the fixed data and visible region
Section titled “1. Plan the fixed data and visible region”In game.js, choose at least three species from your existing Pokédex. Give each fixed record a unique Pokédex number, name, type, starting hit points, and path to its local image. Write down the percentage you want for normal play. The worked example uses 25%; your documented percentage may differ.
Before coding the decision, fill in this small table in your project notes. If you are also completing the Level 2 project assignment, put it under Game-data decisions in project-brief.md.
| Decision | Your value |
|---|---|
| Encounter percentage and decimal threshold | Your whole-number percentage, then divide by 100 |
| Chance values that succeed | From 0 up to, but not including, the threshold |
| Encounter-list length | At least 3 |
| Valid indexes | 0 through length minus 1 |
| Known test species | One unique Pokédex number from the list |
The Module 2 random-result example uses 0.249 and 0.25 to check a 25% boundary. For another percentage, write the two values immediately below and at your own threshold. The value at the threshold must fail when success uses <.
In game.html, give the existing exploration section a stable ID. Add a separate, named encounter section before Current game. Save it with hidden because no encounter exists before JavaScript runs. Include a heading, wild image, wild name and type, wild hit points, and starter details. Give the saved image a valid local src from the encounter list and an empty initial alt; rendering will replace both values with the active Pokémon’s image and name. Add an in-page link to the existing trainer-name field, with text that tells the player to submit trainer setup again to return to the map. The heading needs tabindex="-1" so JavaScript can focus it after revealing the section without making it an extra Tab stop. Read Move focus to a newly shown section before adding this attribute.
In styles.css, style the encounter section with the existing card spacing, keep the image responsive, and give the focused heading a visible outline. The image has fixed pixels, but its text alternative names the wild Pokémon. The status and heading also identify the state without relying on the image or color.
Reload without starting a trainer. The new section is hidden. The map and form behave as they did in Part 8. Inspect the DOM to confirm that the encounter section and heading exist, and that the heading is outside the hidden exploration section.
If it does not work
Section titled “If it does not work”If the encounter section appears on initial load, inspect its saved hidden attribute and any CSS rule that might force it to display. If the heading is inside the exploration section, move it outside before writing the transition code.
Checkpoint: Encounter data and destination region are ready
- What now works
- The fixed species and percentage are documented, and a hidden encounter section has a stable heading and output elements. Part 8 movement still works.
- Files changed
game.js, game.html, styles.css- What remains
- Make one grass-move decision and store its result.
- Next action
- Add a named decision function that returns either null or one fixed species definition.
- If it does not work
- Compare each species number and image path with the Pokédex and assets folder, then inspect the saved hidden attribute.
2. Make one decision after an accepted grass move
Section titled “2. Make one decision after an accepted grass move”Create one named function whose result is either null or one fixed record from the encounter list. In normal mode, create one Math.random() value for the chance. Compare it with the documented percentage divided by 100. If it fails, return null without selecting a species. If it succeeds, create a second random value and use Math.floor(value * list.length) to select one valid index.
Put the function call in moveTrainer() only after the destination has passed the absent and blocked checks, after the new position is stored, and only when the accepted destination is grass. A rejected move does not reach this call. Neither renderMap() nor renderGame() calls it.
Use the returned definition as a template. On success, create a new object for state.activeEncounter; set its current and maximum hit points from the definition’s starting hit points. On failure, leave activeEncounter as null and report that no wild encounter occurred. Set the final status before rendering either result.
| Destination result | Chance checks | Position | Encounter |
|---|---|---|---|
| Outside map or blocked | 0 |
Old square | None |
| Accepted path or Center | 0 |
New square | None |
| Accepted grass, failed chance | 1 |
New square | None |
| Accepted grass, successful chance | 1 |
New square | One stored species |
Temporarily use the boundary values from your planning table in place of the chance value. Test one success and one failure, then restore Math.random(). For success, also test selection values 0 and a value close to 1: both must produce an allowed species, never an index equal to the list length. An accepted path move must not call the decision function. Remove temporary values before continuing.

If it does not work
Section titled “If it does not work”If a failed chance selects a species, return before the second random value. If one move produces several rolls, inspect calls from moveTrainer() and remove any roll from rendering. If the encounter’s hit points change the fixed definition, create a new object instead of assigning the definition object itself to state.
Assistance 1 — Locate the single encounter boundary
Trace North from the Center. findMapTile(1, 2) returns grass. The position changes first; then one encounter decision occurs before renderGame(). A blocked or outside-map proposal returns earlier. A path or Center result never calls the encounter decision.
Assistance 2 — Separate chance from species selection
Ask two questions in order: did the percentage check succeed, and if so, which allowed list item was selected? The second random value exists only on success. Use the list’s current length in the index formula, not a fixed number copied from an example.
Checkpoint: Grass creates zero or one stored encounter
- What now works
- One accepted grass move performs one chance check. Failure keeps exploration active; success stores one allowed species with full starting hit points before rendering.
- Files changed
game.js- What remains
- Render the chosen state, move focus, and make the result repeatable for development tests.
- Next action
- Select the encounter section and its output elements, then switch the visible region from activeEncounter.
- If it does not work
- Trace one accepted grass move through the destination, chance value, returned definition, activeEncounter assignment, and render call.
3. Render the encounter and move focus
Section titled “3. Render the encounter and move focus”Select the two sections, encounter heading, image, and text outputs once beside the existing DOM selections. Add them to the missing-elements guard. In renderGame(), derive section visibility from state.activeEncounter: show exploration when it is null, or show the encounter section when it has a record. Fill the encounter outputs from that stored record and the starter. Keep the existing encounter-output summary in agreement.
The movement buttons already derive their disabled state from state.activeEncounter. Once an encounter starts, the activated button becomes unavailable and its section becomes hidden. After renderGame() has shown the encounter section, call focus() on its tabindex="-1" heading. Do this in the successful movement path, not in every render. Focus stays there if an unrelated render runs again.
A new valid trainer submission already resets activeEncounter to null. Keep that reset and the existing focus return to North. This gives the stage a repeatable restart route before battle actions exist.
Start a trainer and trigger a successful grass decision. The map disappears, the encounter section shows the selected image and hit points, all four movement buttons are disabled, the summary names the same Pokémon, the status announces it, and the encounter heading has visible focus. Press Tab: the next useful stop is the link back to trainer setup. Call renderGame() again in DevTools: identity and hit points stay unchanged. Follow the link and submit the trainer form again: the map returns, the encounter clears, and North receives focus.

If it does not work
Section titled “If it does not work”If focus goes to the page body, move it after the render call and confirm that the heading is visible before focusing it. If the wild image is broken, compare its local path with the assets folder. If the map remains visible, confirm that the visibility condition reads state.activeEncounter, not a new random value.
4. Make the encounter tests repeatable
Section titled “4. Make the encounter tests repeatable”Add one source-code setting near the fixed definitions with three allowed values: "normal", "no-encounter", and "known-encounter". Record the normal value and one known Pokédex number from your encounter list in your project notes. If you are completing the Level 2 assignment, use the Controlled setup routes section of interactive-test-report.md.
Read the setting at the one encounter-decision function. No-encounter mode returns null. Known-encounter mode returns the list definition with the documented number; stop with a clear error if that number matches none or more than one. Normal mode uses the percentage and bounded index. All modes then use the same movement, state, render, status, and focus code. This follows the Module 2 controlled-test pattern.
| Mode | Result after an accepted grass move | Random values |
|---|---|---|
"normal" |
Documented chance; one species on success | One chance value, then one selection value only on success |
"no-encounter" |
No active encounter | 0 |
"known-encounter" |
The documented species from the same list | 0 |
For each controlled mode, save the source setting, reload, start a fresh trainer, and move North from the Center. Repeat that route from a fresh start. Record both results. Restore "normal", reload, inspect the saved setting, and confirm one normal grass move reaches the ordinary decision. Keep the chosen percentage unchanged throughout.
In no-encounter mode, two fresh North routes stay in exploration and report no encounter. In known-encounter mode, two fresh North routes show the same number, species, and starting hit points. The focused heading and movement lock work in both known runs. In normal mode, a temporary Console trace at the chance line appears once per accepted grass move; remove the trace and verify the saved setting is "normal".
If it does not work
Section titled “If it does not work”If known mode shows no species, compare the number’s type and value with the fixed definitions. If it uses a separate render path, move the mode check into the one decision function and keep the state update after it. If normal play is still controlled, restore the source setting and reload before saving the checkpoint.
Assistance 3 — Plan the decision and transition without syntax
- Read the development mode in one function. Return no definition, the unique known definition, or the normal random result.
- Only an accepted grass move calls that function. Store the new coordinate first.
- If it returns a definition, create a separate encounter record with full hit points. Otherwise keep no active encounter.
- Set one status, render once, then focus the shown heading only for a new encounter.
- Derive visible sections, encounter text, and movement availability from state during rendering.
Assistance 4 — Use a partial decision structure
Complete the missing branches in this outline. Keep the definition lookup and percentage decision in the same named function:
function chooseEncounterDefinition() { if (encounterTestMode === "no-encounter") { return null; }
if (encounterTestMode === "known-encounter") { // Find the one definition with knownEncounterNumber. // Stop with an error if it is absent or duplicated. }
// Reject an unknown mode before normal play. const chanceValue = Math.random(); // Return null when the chance fails. const selectionValue = Math.random(); const encounterIndex = Math.floor(selectionValue * encounterDefinitions.length); return encounterDefinitions[encounterIndex];}Call this function in the accepted grass branch of moveTrainer(). Keep the encounter object creation and the single render call outside this function.
Assistance 5 — Review one complete stage 09 implementationExample solution
This is one reference implementation. It chooses 25% and Bounsweet, Mareep, and Misdreavus. Your species, number, and percentage choices may differ. Keep the previous map, trainer, movement, and Pokédex code.
Add the following fixed data near the other definitions in game.js:
const encounterChancePercent = 25;const encounterTestMode = "normal"; // "normal", "no-encounter", or "known-encounter"const knownEncounterNumber = 179;
const encounterDefinitions = [ { number: 761, name: "Bounsweet", type: "Grass", maximumHitPoints: 24, image: "assets/bounsweet.png", }, { number: 179, name: "Mareep", type: "Electric", maximumHitPoints: 30, image: "assets/mareep.png", }, { number: 200, name: "Misdreavus", type: "Ghost", maximumHitPoints: 28, image: "assets/misdreavus.png", },];Add a stable ID to the existing exploration section. Put this sibling section before the current-game summary in game.html:
<section class="battle" id="battle-section" aria-labelledby="battle-heading" hidden> <h2 id="battle-heading" tabindex="-1">Wild encounter</h2> <p>A wild Pokémon appeared. To return to the map, <a href="#trainer-name">go to trainer setup</a> and select Start game again.</p> <img id="wild-image" class="wild-image" src="assets/bounsweet.png" width="475" height="475" alt=""> <p id="wild-name" class="wild-name"></p> <p>Wild hit points: <span id="wild-hit-points"></span></p> <p>Your starter: <span id="battle-starter"></span></p></section>Select #overworld-section, #battle-section, #battle-heading, #wild-image, #wild-name, #wild-hit-points, and #battle-starter beside the existing DOM selections. Add every selection to the missing-elements guard. The same element names below refer to those selections.
function chooseEncounterDefinition() { if (encounterTestMode === "no-encounter") { return null; }
if (encounterTestMode === "known-encounter") { let knownDefinition = null;
for (const definition of encounterDefinitions) { if (definition.number === knownEncounterNumber) { if (knownDefinition !== null) { throw new Error("Known encounter number matches more than one species."); } knownDefinition = definition; } }
if (knownDefinition === null) { throw new Error("Known encounter number is absent from the encounter list."); }
return knownDefinition; }
if (encounterTestMode !== "normal") { throw new Error(`Unknown encounter test mode: ${encounterTestMode}`); }
const chanceValue = Math.random();
if (chanceValue >= encounterChancePercent / 100) { return null; }
const selectionValue = Math.random(); const encounterIndex = Math.floor(selectionValue * encounterDefinitions.length); return encounterDefinitions[encounterIndex];}Replace the previous non-Center status branch in moveTrainer() with three cases: Center, grass, and other accepted terrain. The following code begins after the accepted coordinate and terrain lookup:
if (destination.terrain === "center") { const neededHealing = state.starter.currentHitPoints < state.starter.maximumHitPoints; state.starter.currentHitPoints = state.starter.maximumHitPoints; if (neededHealing) { state.statusMessage = `${state.starter.name} was healed at the Pokémon Center.`; } else { state.statusMessage = `${state.starter.name} is already at full hit points.`; }} else if (destination.terrain === "grass") { const definition = chooseEncounterDefinition();
if (definition === null) { state.statusMessage = `Moved ${direction} to tall grass (row ${proposedRow + 1}, column ${proposedColumn + 1}). No wild encounter.`; } else { state.activeEncounter = { number: definition.number, name: definition.name, type: definition.type, image: definition.image, currentHitPoints: definition.maximumHitPoints, maximumHitPoints: definition.maximumHitPoints, }; state.statusMessage = `A wild ${definition.name} appeared!`; }} else { state.statusMessage = `Moved ${direction} to ${terrain.name} (row ${proposedRow + 1}, column ${proposedColumn + 1}).`;}
renderGame();
if (state.activeEncounter !== null) { battleHeading.focus();}In renderGame(), after the existing encounter summary and before the caught count, derive the two visible regions and fill the active encounter view:
overworldSection.hidden = state.activeEncounter !== null;battleSection.hidden = state.activeEncounter === null;
if (state.activeEncounter !== null) { wildImage.setAttribute("src", state.activeEncounter.image); wildImage.setAttribute("alt", state.activeEncounter.name); wildName.textContent = `${state.activeEncounter.name} — ${state.activeEncounter.type}`; wildHitPoints.textContent = `${state.activeEncounter.currentHitPoints} of ${state.activeEncounter.maximumHitPoints}`; battleStarter.textContent = `${state.starter.name}: ${state.starter.currentHitPoints} of ${state.starter.maximumHitPoints} hit points`;}Keep the existing movement-button update at the end of renderGame(). Give .battle the same card rules as .overworld, then add a responsive image and a visible heading focus outline in styles.css:
.wild-image { display: block; inline-size: 100%; max-inline-size: 12rem; block-size: auto; margin-block: var(--space-md);}
.wild-name { font-size: 1.25rem; font-weight: bold;}
#battle-heading:focus { outline: 3px solid var(--color-focus); outline-offset: 3px;}Save encounterTestMode as "normal" after testing. The complete result includes the existing renderGame() call and North focus in valid trainer setup; neither needs a new listener.
Verify the complete stage
Section titled “Verify the complete stage”Self-check
Complete these checks against the required result.
- Confirm the fixed list has at least three unique Pokédex numbers with valid local images and documented starting hit points.
- From fresh setup, move onto path and Center squares. Neither calls the encounter decision.
- Try an outside-map and a blocked move. Both keep the old position and make no encounter decision.
- Test chance values immediately below and at the documented threshold. Only the value below succeeds; restore Math.random afterward.
- Test selection values 0 and close to 1. Both select a valid list item; restore Math.random afterward.
- In controlled no-encounter mode, repeat a fresh North grass move twice. Both keep exploration visible and report no encounter.
- In controlled known-encounter mode, repeat a fresh North grass move twice. Both show the same species, starting hit points, status, disabled movement, and focused heading.
- While the encounter is active, call renderGame again. The wild species and hit points do not change, and no random value is created.
- From the encounter heading, press Tab to reach the trainer-setup link. Follow it and start a valid trainer again. The map returns, the encounter clears, and North is focused.
- Restore normal mode, reload, inspect the saved setting, and check that one accepted grass move calls the ordinary chance decision once.
- Check the game near 320 CSS pixels and at 200% browser zoom. Text wraps, the image fits, and there is no horizontal page scroll.
- Check the browser Console for errors and test the transition with pointer, Enter, and Space activation of North in separate fresh runs.
Safe stopping point
Section titled “Safe stopping point”Save a Git checkpoint after the complete self-check passes, with "normal" restored and temporary test values removed. The map can now start and display one stable wild encounter. A new valid trainer setup resets it. Continue to Build the battle loop when that article is available; it will add the actions that resolve the encounter.