Pokémon Browser Game: Render the trainer position
Outcome
Section titled “Outcome”Show one trainer sprite on the map tile named by state.mapPosition. Before trainer setup, show no sprite. Keep the terrain letter visible in the tile and report the trainer’s row, column, and terrain in the current-game summary.
What you will practice
- Use one local image file for a trainer marker without adding animation or a sprite-sheet API.
- Compare the current position with each map record while rendering the existing list.
- Derive the marker and location text from state instead of storing a second position.
- Give the marked tile a complete accessible name while keeping the image decorative.
- Test repeated renders and temporary valid positions before adding movement controls.
Why this stage matters
Section titled “Why this stage matters”The next stage will change state.mapPosition after an accepted move. This stage makes the map and text respond to that value first. If the position changes, the next render will remove the old marker and create one marker at the new coordinate.
The data route is:
state.mapPosition -> renderGame() -> renderMap() and the current-game summaryThe sprite is a DOM result. Do not add a second position to state, store the previous tile, or move an existing image element between tiles.
What is new and what is reused
Section titled “What is new and what is reused”- New in this Guided Build: One local trainer image, a coordinate lookup for the location text, a position comparison inside
renderMap(), and one render call that updates both the map and summary. - Reused from Module 2: Objects,
null,for...of, conditions,document.createElement,setAttribute,textContent,append,replaceChildren, and the state-to-render pattern. Review the coordinate lookup pattern and rendering from state as needed. - Reused from earlier project stages: The fixed 25-record map, terrain definitions, trainer setup,
state.mapPosition, the map list, terrain symbols, and the current-game summary.
Starting point
Before you start
- The overworld-map article passes its complete self-check.
- game.html has the four-entry legend, empty overworld list in saved HTML, and current-game summary.
- game.js has mapTiles, findTerrainDefinition(), renderMap(), renderGame(), and state.mapPosition.
- A valid trainer setup stores JavaScript row 2, column 2 and shows row 3, column 3 in the summary.
- styles.css shows the map as five columns at narrow and wide widths.
- The project has an assets folder and a verified overworld-map Git checkpoint.
- Current state
- The map always renders 25 terrain tiles. Trainer setup changes the position summary, but no tile shows where the trainer is.
- First action
- Download the trainer PNG below and save it as assets/trainer.png in the student project.
- First checkpoint
- The local PNG opens in the browser, and the map still renders 25 terrain tiles before you change JavaScript.
- Help trigger
- Use the nearest assistance level or ask for help if the image URL fails, the marker count is not zero before setup or one after setup, the marked coordinate disagrees with the summary, a second render duplicates a marker, terrain letters disappear, or the map overflows at a narrow width.
Required result
Section titled “Required result”Complete this stage when:
assets/trainer.pngis one local, square character image with a source the project can use;- the map reading instruction explains the trainer sprite as well as the terrain letters;
- before valid trainer setup, the map has 25 terrain tiles and no trainer sprite;
- after valid trainer setup, exactly one trainer sprite appears on the Center tile at JavaScript row
2, column2; - the marked tile’s accessible name states its one-based row and column, terrain, and that the trainer is there;
- the current-game summary names the terrain at
state.mapPositionand reports the same one-based coordinates; - each call to
renderGame()updates both the summary and the map from the current state; - repeated rendering leaves 25 tiles and at most one sprite;
- temporary valid position changes move the sprite and update the summary without changing the fixed map records;
- the terrain symbol and legend stay visible, the image adds no keyboard stop, and the result works without relying only on color or the sprite; and
- trainer setup and the existing map checks still pass at narrow width, 200% zoom, and in the Console.
Movement controls, animation, sprite sheets, direction-specific images, and new game-state properties belong to later work.
1. Add one local trainer image
Section titled “1. Add one local trainer image”Download the trainer PNG below. It is Tiles/tile_0085.png from Kenney’s Tiny Dungeon pack, which Kenney licenses under CC0. The file is one complete 16 × 16 character, so this stage needs no cropping or sprite-sheet code.
Save the downloaded file in the student project’s assets folder as trainer.png. Use the same still image for every position in this stage.
You may use another square PNG when you have permission to use it. Keep the same local filename so the JavaScript does not depend on the asset’s original name or a remote image URL. Record the source link in the project README even when attribution is not required; it lets you find the asset again.
Open assets/trainer.png in the browser or an image viewer. Confirm that it contains one character, has a transparent background, and remains recognizable at about 20 × 20 CSS pixels. Confirm that the map still renders before you change game.js.
If it does not work
Section titled “If it does not work”If the image shows several characters, choose an individual character file instead of a complete sheet. If the browser cannot open it, check the filename, extension, and assets folder inside the project.
Checkpoint: The trainer image is ready
- What now works
- One local PNG has a stable path and can represent the trainer without sprite-sheet code.
- Files changed
assets/trainer.png, README.md- What remains
- Connect the image to the map-position state and report its terrain.
- Next action
- Open game.html and update the map reading instruction before changing renderMap().
- If it does not work
- Open the image directly and compare its saved path with assets/trainer.png.
2. Explain the marker and keep the terrain visible
Section titled “2. Explain the marker and keep the terrain visible”In game.html, update the existing paragraph under Overworld map. Keep its top-left-to-bottom-right reading instruction. Add a sentence such as: The trainer image marks your current tile; the letter still identifies its terrain.
Do not add a saved img inside the empty ol. The map list is still produced by JavaScript. The image will appear only when state.mapPosition has a value.
In styles.css, let the existing tile Flexbox place a terrain letter and image in a column. Add flex-direction: column and a small gap to .map-tile. Add this focused image rule:
.trainer-sprite { display: block; inline-size: 1.25rem; block-size: 1.25rem;}The fixed image size makes one small marker fit inside a tile. The terrain letter remains readable when the marker is present.
Reload the page. The new reading instruction appears, the map still has 25 letters, and there is no marker before setup. The layout should stay at five columns.
If it does not work
Section titled “If it does not work”If the instruction appears after the map, move the paragraph in the HTML source. If a tile becomes too tall later, inspect the tile’s minimum height and the image rule before changing the map tracks.
3. Render one marker from the current position
Section titled “3. Render one marker from the current position”Open game.js. Keep the existing overworldMap.replaceChildren() at the start of renderMap(). It clears the old tiles and any old marker before the loop creates the current result.
For each mapTile, compare its two coordinates with state.mapPosition. Check for null first. Both numbers must match:
const trainerHere = state.mapPosition !== null && mapTile.row === state.mapPosition.row && mapTile.column === state.mapPosition.column;Use that result after the terrain lookup and before appending the tile:
- Keep the existing terrain class and terrain symbol.
- Give every tile its current accessible name. When
trainerHereis true, add Trainer here. to that name. - Set the tile’s
textContentto its terrain symbol before adding the image. - When
trainerHereis true, create oneimg. Set itssrctoassets/trainer.png, itsclasstotrainer-sprite, and itsaltto an empty string. Append it to that tile. - Append the tile to the map list as before.
The empty image alternative prevents a second announcement of the trainer. The list item’s accessible name already reports the trainer, coordinate, and terrain. Keep the visible symbol so the image does not cover the terrain information.
For now, run renderMap() in the Console after a valid trainer setup. Exactly one image should appear on the Center tile. Run it again. The list still contains 25 tiles and one image.
If it does not work
Section titled “If it does not work”- If no image appears, inspect
state.mapPositionand the coordinate comparison before changing CSS. - If the image URL fails, inspect the rendered
imgin DevTools and compare itssrcwith the local file path. - If the Center’s
Cdisappears, set the tile’stextContentbefore you append the image. - If a second render adds another image, confirm that
replaceChildren()runs before the loop.
Assistance 1 — Locate the position comparison
Put the comparison inside the existing for...of loop. Each loop visit already has one mapTile with its row and column. The current position comes from state.mapPosition.
Assistance 2 — Use the two-coordinate match
A matching row alone can mark five tiles. A matching column alone can mark five tiles. Use && so both comparisons must be true, and test state.mapPosition !== null before reading either property.
Assistance 3 — Order the tile work
Read the record, find its terrain, compare its position, create and name the list item, put in the terrain symbol, conditionally append one image, then append the list item.
Checkpoint: A valid position creates one visible marker
- What now works
- The starting Center tile keeps its C symbol and gains one trainer image and a complete accessible name. Repeated map rendering still produces 25 tiles and one marker.
- Files changed
game.html, game.js, styles.css- What remains
- Make the current-game text name the terrain and connect every game render to the map renderer.
- Next action
- Add the coordinate lookup beside findTerrainDefinition() in game.js.
- If it does not work
- Trace the Center record through the coordinate comparison, tile name, textContent, image creation, and append calls.
4. Report the terrain and render both results together
Section titled “4. Report the terrain and render both results together”Add findMapTile(row, column) near findTerrainDefinition(). Reuse the Module 2 coordinate lookup: loop through mapTiles, return the record whose row and column both match, and return null if no record matches.
In the existing non-null position branch of renderGame():
- Find the map record at
state.mapPosition. - If no record matches, throw an error that names the invalid position.
- Find the terrain definition for that record. Reject an unknown terrain ID as
renderMap()already does. - Keep the existing one-based
visibleRowandvisibleColumncalculations. - Set
positionOutput.textContentto the terrain name and those visible coordinates. At the starting tile it should read Pokémon Center — row 3, column 3.
Do not set the position text to a fixed Center label. When later movement changes the row and column, the terrain name must change with them.
Call renderMap() once at the end of renderGame(), after the summary and status text updates. Remove the separate renderMap() call from the file’s initialization. Keep the existing trainer submit handler’s renderGame() call. The end of game.js now registers the listener and calls renderGame() once:
trainerForm.addEventListener("submit", handleTrainerSubmit);
renderGame();This gives both the initial page and later valid trainer setups one render route. A future movement handler can change state.mapPosition and call renderGame() without learning a new rendering rule.
Reload. Before setup, the map has 25 tiles, no trainer image, and the position summary says Not set. Submit a valid fictional trainer name and starter. The Center now has one image, and the summary reports Pokémon Center — row 3, column 3 without a Console error.

If it does not work
Section titled “If it does not work”- If the map is empty on reload, confirm that
renderMap()runs fromrenderGame()and that the finalrenderGame()call remains. - If the marker appears only after a manual Console call, confirm that the trainer handler still calls
renderGame(). - If the terrain text is wrong, inspect the record returned by
findMapTile()and the definition returned byfindTerrainDefinition().
Checkpoint: The summary and map share one render route
- What now works
- Initial loading and valid trainer setup each render the map and summary from the same current state. The text names the terrain at the stored coordinate.
- Files changed
game.js- What remains
- Test another valid position and repeat the trainer setup and layout checks.
- Next action
- Start a valid trainer, then temporarily change state.mapPosition in the Console.
- If it does not work
- Check the lookup result for row 2, column 2 before inspecting the displayed terrain text.
5. Test position changes without movement controls
Section titled “5. Test position changes without movement controls”Start a valid trainer first. In the browser Console, set one temporary position and render:
state.mapPosition = { row: 1, column: 2 };renderGame();The image moves to JavaScript row 1, column 2. The tile’s name says Row 2, column 3: Tall grass. Trainer here. The current-game summary says Tall grass — row 2, column 3. The Center keeps its C and no longer contains an image.

Test a path position in the same way with { row: 2, column: 3 }. Then restore the Center:
state.mapPosition = { row: 2, column: 2 };renderGame();These Console changes are temporary development tests, not movement controls. Reloading clears them. Do not save test assignments in game.js.
Run these count checks after each render:
document.querySelectorAll(".map-tile").lengthdocument.querySelectorAll(".trainer-sprite").lengthThe results are 25 and 1 after setup. Call renderGame() again and expect the same counts. Reload and expect 25 and 0 before setup.
Also inspect the marked tile’s accessible name, check that no tile or image enters the Tab order, and confirm that the terrain letter and summary remain readable if you hide the image in DevTools. Test near 320px and at 200% browser zoom in both light and dark browser themes.
If it does not work
Section titled “If it does not work”If the old and new positions both show images, verify that replaceChildren() runs before every map loop. If the sprite moves but the text stays on the Center, replace the fixed Center label with the two lookups in renderGame(). If the map clips, check the image size, tile gap, and five flexible Grid tracks.
Optional assistance for the complete change
Section titled “Optional assistance for the complete change”Assistance 4 — Use a partial renderer structure
Keep the existing terrain lookup, tile classes, and final list append. Add this partial block inside the for...of loop after you create and name tileElement:
tileElement.textContent = terrainDefinition.symbol;
if (trainerHere) { // Extend the tile's accessible name with "Trainer here." const trainerImage = document.createElement("img"); // Set the image's class, local src, and empty alt. tileElement.append(trainerImage);}Finish the attributes before you test. At the end of renderGame(), call renderMap().
Assistance 5 — Review the complete trainer-position changesExample solution
Keep the unchanged map data, trainer handler, and before-setup text from the previous stage. Add this lookup:
function findMapTile(row, column) { for (const mapTile of mapTiles) { if (mapTile.row === row && mapTile.column === column) { return mapTile; } }
return null;}Replace renderMap() with this version:
function renderMap() { overworldMap.replaceChildren();
for (const mapTile of mapTiles) { const terrainDefinition = findTerrainDefinition(mapTile.terrain);
if (terrainDefinition === null) { throw new Error(`Unknown terrain: ${mapTile.terrain}`); }
const trainerHere = state.mapPosition !== null && mapTile.row === state.mapPosition.row && mapTile.column === state.mapPosition.column; const tileElement = document.createElement("li"); const visibleRow = mapTile.row + 1; const visibleColumn = mapTile.column + 1; const tileName = `Row ${visibleRow}, column ${visibleColumn}: ${terrainDefinition.name}.`;
tileElement.setAttribute( "class", `map-tile map-tile--${mapTile.terrain}`, ); tileElement.setAttribute("aria-label", tileName); tileElement.textContent = terrainDefinition.symbol;
if (trainerHere) { tileElement.setAttribute("aria-label", `${tileName} Trainer here.`); const trainerImage = document.createElement("img"); trainerImage.setAttribute("class", "trainer-sprite"); trainerImage.setAttribute("src", "assets/trainer.png"); trainerImage.setAttribute("alt", ""); tileElement.append(trainerImage); }
overworldMap.append(tileElement); }}In renderGame(), replace the fixed Center position text in its non-null branch with:
const currentTile = findMapTile( state.mapPosition.row, state.mapPosition.column,);
if (currentTile === null) { throw new Error( `No map tile at row ${state.mapPosition.row}, column ${state.mapPosition.column}.`, );}
const currentTerrain = findTerrainDefinition(currentTile.terrain);
if (currentTerrain === null) { throw new Error(`Unknown terrain: ${currentTile.terrain}`);}
const visibleRow = state.mapPosition.row + 1;const visibleColumn = state.mapPosition.column + 1;
positionOutput.textContent = `${currentTerrain.name} — row ${visibleRow}, column ${visibleColumn}`;Call renderMap() at the end of renderGame() after the existing status text assignment. Remove the separate initialization call to renderMap(). Keep the renderGame() call at the end of the file and in the valid trainer handler. Add the .trainer-sprite CSS rule from Section 2 and the new reading sentence in game.html.
Verify the stage
Section titled “Verify the stage”Start each independent test from a fresh reload unless the row gives another starting state.
| Test | Action | Expected result |
|---|---|---|
| Before setup | Reload game.html |
25 terrain tiles, no trainer sprite, position Not set., no Console error |
| Invalid setup | Submit empty or whitespace-only trainer name | Existing field error and focus; no sprite; summary unchanged |
| Valid setup | Submit a fictional name and each starter in separate tests | One sprite on the Center, Center letter visible, matching row 3 and column 3 summary |
| Repeated render | Call renderGame() twice after valid setup |
25 tiles, one sprite, unchanged map records |
| Changed position | Use the tall-grass and path Console tests | Old tile loses the image; one new tile gains it; name and summary match |
| Reload | Reload after a temporary change | Before-setup state and zero sprites return |
| Accessibility | Inspect the marked tile and use Tab | Complete tile name; image is decorative; no map keyboard stop |
| Layout | Check narrow width, 200% zoom, and light and dark themes | Five columns, visible letter and sprite, no clipping or horizontal scrolling |
Validate game.html with the established HTML process. Review styles.css in VS Code Problems. Repeat the map counts, terrain names, and trainer form regression checks from the previous stages. Inspect the Console and remove temporary tests from saved files.
Self-check
Complete these checks against the required result.
- Confirm that one local trainer.png file opens and its source is recorded in the README.
- Confirm that saved game.html keeps the map list empty and explains the sprite and terrain letter.
- Reload before setup and confirm 25 tiles, zero images, Not set., and no Console error.
- Complete valid trainer setup and confirm one image at row 2, column 2 in JavaScript and row 3, column 3 in the interface.
- Inspect the Center tile name and confirm it states the coordinate, terrain, and Trainer here.
- Confirm that the terrain symbol and legend remain visible and useful without the image.
- Call renderGame twice and confirm 25 tiles and exactly one image.
- Temporarily test one grass and one path coordinate, then restore the Center and confirm that image and summary agree after each render.
- Repeat invalid trainer setup and keyboard focus checks without changing the current game.
- Check the map near 320px and at 200% zoom in light and dark themes.
- Validate HTML, review CSS diagnostics, and confirm a clear browser Console.
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 map now renders terrain and one trainer position from the same state. The summary reports the matching terrain and one-based coordinates. The image has no separate stored position, and no movement event is active.
Continue to Move one step when that article is available. Its first action will propose a new coordinate and check the destination before changing state.mapPosition.