Skip to content

Pokémon Browser Game: Render the trainer position

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.

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 summary

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

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

Complete this stage when:

  • assets/trainer.png is 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, column 2;
  • 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.mapPosition and 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.

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

styles.css — trainer image size
.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 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:

game.js — compare one map record with state
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:

  1. Keep the existing terrain class and terrain symbol.
  2. Give every tile its current accessible name. When trainerHere is true, add Trainer here. to that name.
  3. Set the tile’s textContent to its terrain symbol before adding the image.
  4. When trainerHere is true, create one img. Set its src to assets/trainer.png, its class to trainer-sprite, and its alt to an empty string. Append it to that tile.
  5. 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 no image appears, inspect state.mapPosition and the coordinate comparison before changing CSS.
  • If the image URL fails, inspect the rendered img in DevTools and compare its src with the local file path.
  • If the Center’s C disappears, set the tile’s textContent before 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():

  1. Find the map record at state.mapPosition.
  2. If no record matches, throw an error that names the invalid position.
  3. Find the terrain definition for that record. Reject an unknown terrain ID as renderMap() already does.
  4. Keep the existing one-based visibleRow and visibleColumn calculations.
  5. Set positionOutput.textContent to 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:

game.js — initial render
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.

Sample game screenshot with one trainer sprite beside C on the Center tile. The Current game summary reports Pokémon Center at row 3, column 3.
After valid setup, the sprite and C share the Center tile. The summary names Pokémon Center at row 3, column 3.
Sample game screenshot with one trainer sprite beside C on the Center tile. The Current game summary reports Pokémon Center at row 3, column 3.
  • If the map is empty on reload, confirm that renderMap() runs from renderGame() and that the final renderGame() 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 by findTerrainDefinition().

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:

Console — test a tall-grass position
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.

Sample game screenshot with the trainer sprite on a G tile at row 2, column 3. The Center keeps C, and the summary reports Tall grass at the trainer's position.
The temporary state change moves the sprite to Tall grass at row 2, column 3. The Center keeps C, and the summary reports the new terrain.
Sample game screenshot with the trainer sprite on a G tile at row 2, column 3. The Center keeps C, and the summary reports Tall grass at the trainer's position.

Test a path position in the same way with { row: 2, column: 3 }. Then restore the Center:

Console — restore the starting position
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:

Console — check the map and marker counts
document.querySelectorAll(".map-tile").length
document.querySelectorAll(".trainer-sprite").length

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

game.js — partial marker block
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:

game.js — coordinate 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:

game.js — map renderer with one trainer marker
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:

game.js — location text inside renderGame()
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.

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.

  1. Confirm that one local trainer.png file opens and its source is recorded in the README.
  2. Confirm that saved game.html keeps the map list empty and explains the sprite and terrain letter.
  3. Reload before setup and confirm 25 tiles, zero images, Not set., and no Console error.
  4. Complete valid trainer setup and confirm one image at row 2, column 2 in JavaScript and row 3, column 3 in the interface.
  5. Inspect the Center tile name and confirm it states the coordinate, terrain, and Trainer here.
  6. Confirm that the terrain symbol and legend remain visible and useful without the image.
  7. Call renderGame twice and confirm 25 tiles and exactly one image.
  8. Temporarily test one grass and one path coordinate, then restore the Center and confirm that image and summary agree after each render.
  9. Repeat invalid trainer setup and keyboard focus checks without changing the current game.
  10. Check the map near 320px and at 200% zoom in light and dark themes.
  11. Validate HTML, review CSS diagnostics, and confirm a clear browser Console.

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.