Pokémon Browser Game: Render the overworld map
Outcome
Section titled “Outcome”Add a fixed 5 × 5 overworld map to the Game page. JavaScript will store 25 tile records and create one list item for each record. CSS Grid will place those items into five columns.
The completed map will show a Pokémon Center, paths, tall grass, and blocked terrain. Each terrain type will use a letter and a legend, so color is not the only source of meaning.
What you will practice
- Represent a rectangular map as one ordered array of row, column, and terrain records.
- Keep terrain definitions separate from the map records that refer to them.
- Render one DOM element for each map record with createElement, textContent, append, and a for...of loop.
- Use replaceChildren to make repeated map renders produce one stable result.
- Arrange 25 rendered tiles with one five-column CSS Grid.
- Communicate terrain with visible labels, accessible names, and color.
- Verify record order, rendered counts, responsive behavior, and Console results before adding a trainer marker.
Why this stage separates the map from movement
Section titled “Why this stage separates the map from movement”Movement needs a reliable answer to two questions: which coordinates exist, and what terrain does each coordinate contain? This stage creates and verifies that map definition before a trainer marker or movement event depends on it.
The program will follow this route:
fixed terrain definitions + ordered map records -> renderMap() -> 25 list items in the DOM -> five-column CSS GridNo click or keyboard event changes the map in this article. The next article will read the existing state.mapPosition value and add one trainer marker to the matching tile.
What is new and what is reused
Section titled “What is new and what is reused”- New: A terrain-definition array, a 25-record map definition, row-major order in the game, an overworld list, terrain lookup, and a dedicated
renderMap()function. - Reused: The completed trainer setup, arrays, objects, properties, stable IDs,
for...of,querySelector, missing-element guards, functions, conditions, template literals,setAttribute,textContent, CSS custom properties, Flexbox, CSS Grid, narrow-first testing, and Console checks.
Use these earlier Guided Build and Module 2 sections when each pattern enters the game:
- Identify the Grid relationship reviews the Grid container, direct Grid items, and tracks already used on the landing page.
- Render the task records introduces
replaceChildren,for...of,document.createElement,textContent, andappend. - Find one record by a stable identity provides the lookup pattern used for terrain definitions.
- Represent a rectangular grid as records introduces row, column, content, row-major order, and coordinate lookup.
Keep the interface-state lesson open while you work. This article applies its general patterns to the game but does not introduce a new JavaScript API.
Starting point
Before you start
- The trainer-setup article passes its complete self-check.
- game.html contains the introduction, trainer form, and current-game summary.
- game.js contains the fixed starter definitions, starting position, state object, guarded DOM selections, trainer handler, and renderGame function.
- A valid setup starts at row 2, column 2 in JavaScript and reports row 3, column 3 in the interface.
- styles.css contains the working trainer form, game summary, shared page styles, and earlier Grid rules.
- The repository contains the verified trainer-setup checkpoint and has no unexplained changes.
- Current state
- Trainer setup creates one current game and stores its starting coordinate. The Game page does not yet show which terrain exists at that coordinate or anywhere else.
- First action
- Open game.html, game.js, and styles.css. Draw the documented 5 × 5 map as five rows of five terrain codes before you edit the files.
- First checkpoint
- The Game page contains a labeled overworld section, a complete terrain legend, and one empty ordered-list region ready for JavaScript-rendered tiles.
- Help trigger
- Open the nearest assistance level or ask for help if the map has a different number of records or tiles than expected, a coordinate appears twice, the Center is not at row 2 and column 2, a terrain ID has no matching definition, a second render duplicates tiles, Grid does not make five columns, terrain depends only on color, or the page gains horizontal scrolling.
Required result
Section titled “Required result”Complete this article when:
game.htmlkeeps the verified trainer setup and adds one overworld section between trainer setup and the current-game summary;- the overworld section has a visible heading, a short reading instruction, a complete terrain legend, and an empty ordered list with the ID
overworld-map; - the legend identifies
Cas Pokémon Center,Pas path,Gas tall grass, andXas blocked terrain; game.jsstores four fixed terrain definitions with stableid,name, andsymbolproperties;- one
mapTilesarray contains exactly 25 records in row-major order; - every map record contains one zero-based
row, one zero-basedcolumn, and one stableterrainID; - rows and columns cover
0through4, each coordinate appears once, and row2, column2contains the Pokémon Center; - the required map contains 1 Center tile, 10 path tiles, 10 tall-grass tiles, and 4 blocked tiles;
- the script selects and guards the map list as part of the page’s stable DOM contract;
renderMap()clears the previous rendered result, finds the matching terrain definition, and creates one list item for every map record;- each rendered tile receives its terrain class, visible symbol, and a full accessible name with one-based row and column values;
- calling
renderMap()more than once still leaves exactly 25 tiles; - CSS Grid arranges the tiles in five equal flexible columns without changing the row-major source order;
- the legend and tile symbols keep the map understandable without color;
- trainer setup still passes its invalid and valid routes;
- no trainer marker, movement control, position lookup, encounter, battle, persistence, API, framework, or advanced Grid helper is added; and
- the page passes HTML, CSS, JavaScript, keyboard, narrow-width, zoom, and Console checks.
1. Fix the coordinate and terrain contract
Section titled “1. Fix the coordinate and terrain contract”Use zero-based coordinates in JavaScript:
- the top-left tile is row
0, column0; - row numbers increase from top to bottom;
- column numbers increase from left to right; and
- the bottom-right tile is row
4, column4.
The interface will add 1 when it gives a coordinate an accessible name. A JavaScript coordinate of row 0, column 0 will therefore be announced as Row 1, column 1.
Use these four stable terrain IDs:
| Terrain ID | Visible symbol | Visible legend label |
|---|---|---|
center |
C |
Pokémon Center |
path |
P |
Path |
grass |
G |
Tall grass |
blocked |
X |
Blocked terrain |
Build this exact first map. The left column identifies the zero-based JavaScript row. The top headings identify the zero-based JavaScript columns.
| Row / column | 0 |
1 |
2 |
3 |
4 |
|---|---|---|---|---|---|
0 |
X |
G |
G |
P |
X |
1 |
P |
P |
G |
P |
G |
2 |
G |
P |
C |
P |
G |
3 |
G |
P |
P |
P |
G |
4 |
X |
G |
G |
P |
X |
Read the table in row-major order: finish row 0 from left to right, then continue with row 1. Keep the same order through row 4.
Before you write JavaScript, confirm these facts from the table:
- Five rows each contain five tiles.
- The four corners contain blocked terrain.
- The Center occupies JavaScript row
2, column2. - The table contains 1
C, 10Psymbols, 10Gsymbols, and 4Xsymbols.
If it does not work
Section titled “If it does not work”Mark off one complete row at a time. Do not count by color. Count the visible terrain symbols and confirm that each row has columns 0 through 4 before you continue.
Assistance 1 — Read one coordinate before reading the complete map
Start at row 0, column 0. The table contains X there, so the matching record will use terrain: “blocked”. Move one column to the right and identify the row, column, and terrain again.
Assistance 2 — Trace the Center across three representations
The table places C at row 2, column 2. The map record must use those two numbers and the terrain ID center. The rendered tile will show C and expose the one-based name Row 3, column 3: Pokémon Center.
Assistance 3 — Convert the table one row at a time
Treat each table row as one five-record batch. Keep the row number unchanged inside that batch and increase the column from 0 to 4. Finish and verify one batch before starting the next row.
2. Add the static overworld region
Section titled “2. Add the static overworld region”In game.html, add one section between the trainer-setup section and the current-game summary. Give it the class overworld and label it with a visible h2 whose ID is overworld-heading.
The section needs four parts in this source order:
| Part | Required contract |
|---|---|
| Heading | h2 text Overworld map with ID overworld-heading |
| Reading instruction | State that the map is read from top left to bottom right and uses both letters and color |
| Legend | A ul with the accessible name Map legend and four visible entries for C, P, G, and X |
| Map output | An empty ol with class overworld-map, ID overworld-map, and accessible name Five by five overworld map |
Use one span for each legend symbol. Give every symbol the shared class map-key and its matching modifier class:
map-key--centermap-key--pathmap-key--grassmap-key--blocked
Keep the symbol as visible text beside its full label. Supporting technology will encounter the same letter-and-label relationship that a sighted visitor can read.
Keep the map list empty in saved HTML. JavaScript owns its 25 list items because they come from the map definition. Do not add buttons, links, tabindex, or event attributes to the map tiles. The map is a static result at this checkpoint.
Save and reload before changing game.js.
- Confirm that the overworld heading and all four legend entries appear between trainer setup and the current-game summary.
- Inspect the map list in DevTools and confirm that it exists but contains no list item yet.
- Use the accessibility tree or browser inspector to confirm the section uses its visible heading and the two lists have useful names.
- Press Tab through the page. The empty map must not add a keyboard stop.
The existing script will report its usual trainer state. An empty map is expected until renderMap() exists.
If it does not work
Section titled “If it does not work”If the section appears after the summary, move the complete section instead of using CSS to reorder it. If a list name is missing, compare the exact aria-label spelling. If a legend entry loses its meaning, confirm that both its symbol and full text label remain inside the same list item.
Assistance 1 — Locate the new section without changing the existing regions
Find the closing tag for the trainer-setup section. Add the complete overworld section after that tag and before the opening tag for game-summary. Leave both existing sections unchanged.
Assistance 2 — Separate the legend from the rendered map
The legend is a saved four-item list that explains stable symbols. The map is an empty ordered list whose 25 items come from JavaScript. They are two different list elements with two different accessible names.
Assistance 3 — Build the region from the outside inward
Create and label the section first. Add the heading and instruction. Build the four legend entries and test their text. Add the empty map list last, then confirm its ID matches the selector that JavaScript will use.
Checkpoint: The page has a stable map region
- What now works
- The overworld section appears in the correct source order, its visible legend explains all four terrain symbols, the empty map list has a stable ID and accessible name, and no noninteractive tile enters the keyboard order.
- Files changed
game.html- What remains
- Represent the four terrain meanings and all 25 map coordinates as fixed JavaScript records.
- Next action
- Open game.js and add the terrain definitions after starterDefinitions.
- If it does not work
- Restore the verified trainer-setup HTML, then add one complete overworld child at a time and repeat the source-order, list-name, and keyboard checks.
3. Define terrain and the 25 map records
Section titled “3. Define terrain and the 25 map records”Add a terrainDefinitions array after starterDefinitions. Each of its four objects needs these properties:
| Property | Purpose |
|---|---|
id |
Matches the stable terrain string stored in a map record |
name |
Supplies the complete human-readable terrain name |
symbol |
Supplies the visible one-letter map label |
Use the exact IDs, names, and symbols from the terrain contract. Keep these definitions outside state. They describe fixed game meanings, not values that change during one game.
After startingPosition, add a mapTiles array. Convert the documented map into 25 objects. Each object must use this property order:
{ row: 0, column: 0, terrain: "blocked" }Record all five tiles in row 0, then all five in row 1, and continue through row 4. Do not generate the records with a loop. The current lesson uses an explicit fixed definition so you can inspect every coordinate and terrain value directly.
Read Represent a rectangular grid as records before you convert the complete table. That section defines row-major order and explains why stable array order helps the render loop.
Check the data before rendering it
Section titled “Check the data before rendering it”Add temporary Console checks while the map is still invisible:
- Log
mapTiles.length. The result must be25. - Log the record at array index
0. It must describe row0, column0, blocked terrain. - Log the record at array index
12. It must describe row2, column2, Center terrain. - Log the record at array index
24. It must describe row4, column4, blocked terrain. - Compare each five-record batch with one row in the map table.
Remove the temporary logs after every result passes.
If it does not work
Section titled “If it does not work”If the length is not 25, count the records inside each row batch before changing any values. If index 12 is not the Center, find the first row that does not contain exactly five records. If a terrain value uses C, P, G, or X, replace the visible code with its stable terrain ID.
Assistance 1 — Separate terrain meaning from tile placement
The terrain-definition array answers what grass means and which symbol represents it. The map array answers where grass appears. Do not repeat the full name and symbol in every tile record.
Assistance 2 — Use the array index as a row-major check
Five records complete each row. Index 0 starts row 0, index 5 starts row 1, index 10 starts row 2, and index 12 is the middle tile.
Assistance 3 — Build and verify five record batches
Write five records for row 0 and compare all three properties with the table. Repeat for one row at a time. After all five batches pass, run the length and index checks before adding DOM code.
4. Select and guard the map output
Section titled “4. Select and guard the map output”Select #overworld-map after the existing gameStatus selection and bind it to overworldMap.
Add overworldMap to the existing missing-elements condition. The guard now covers both trainer setup and the map, so change its error message to:
Required game elements are missing.Reuse Connect HTML to JavaScript through the DOM for the selector contract and Stop when a required element is missing for the guard pattern.
Test the guard deliberately
Section titled “Test the guard deliberately”- Temporarily change the selector to
#overworld-map-test. - Reload and confirm that the Console reports Required game elements are missing.
- Restore
#overworld-map. - Reload and confirm that the error is gone.
Do not continue while the selector is null.
Assistance 1 — Trace the selector contract in both files
In game.html, find the ordered list whose ID is overworld-map. In game.js, confirm that document.querySelector uses #overworld-map. The hash belongs in the selector string, not in the HTML ID.
Assistance 2 — Extend the existing guard instead of adding a second system
Add one more missing-element condition to the current multi-line if. Keep the single specific error after the complete condition. The page has one DOM contract even though it now contains several regions.
5. Render one tile for each record
Section titled “5. Render one tile for each record”Create findTerrainDefinition(terrainId) after findStarterDefinition(). Reuse the same stable-identity pattern:
- visit every record in
terrainDefinitionswithfor...of; - compare the record’s
idwithterrainId; - return the matching record immediately; and
- return
nullafter the loop when no terrain definition matches.
Then add renderMap(). Keep its work in this order:
- Remove all previously rendered map children with
overworldMap.replaceChildren(). - Visit each
mapTileinmapTileswithfor...of. - Find the matching terrain definition.
- If the lookup returns
null, throw an error that includes the unknown terrain value. - Create one
liwithdocument.createElement. - Calculate one-based
visibleRowandvisibleColumnvalues by adding1to the record coordinates. - Set the shared
map-tileclass and the matching terrain modifier class. - Set an accessible name in this format: Row 1, column 1: Blocked terrain.
- Put the terrain symbol into the tile with
textContent. - Append the completed tile to
overworldMap.
Use setAttribute for the complete class value and accessible name. The terrain ID comes from fixed project data and makes modifier classes such as map-tile--grass. Do not use innerHTML.
Read Render the task records beside this work. Its task list and this map use the same browser operations. The records and rendered elements have different content, but the data-to-DOM route is the same.
Call renderMap() once near the end of the file, after event-listener registration and before the existing initialization call to renderGame().
The map is fixed in this stage, so renderMap() stays separate from renderGame(). The trainer-position article will connect the map renderer to changing position state when a visible marker exists.
Check the rendered result
Section titled “Check the rendered result”Reload and run these checks in the Console:
document.querySelectorAll(".map-tile").lengthdocument.querySelectorAll(".map-tile--center").lengthdocument.querySelectorAll(".map-tile--path").lengthdocument.querySelectorAll(".map-tile--grass").lengthdocument.querySelectorAll(".map-tile--blocked").lengthThe results must be 25, 1, 10, 10, and 4 in that order.
Call renderMap() a second time in the Console. The complete tile count must remain 25, not become 50.
Inspect the first, middle, and last list items in DevTools:
- first: Row 1, column 1: Blocked terrain.;
- middle: Row 3, column 3: Pokémon Center.; and
- last: Row 5, column 5: Blocked terrain..
Test the unknown-terrain branch
Section titled “Test the unknown-terrain branch”Temporarily change one map record’s terrain to water. Reload and confirm that the Console reports Unknown terrain: water. Restore the documented terrain and confirm that all 25 tiles render again.
If it does not work
Section titled “If it does not work”- If no tile appears, confirm that
renderMap()runs after the selector guard. - If the list grows after a second call, confirm that
replaceChildren()runs before the loop. - If every tile has the same symbol, confirm that the loop uses the current
mapTile.terrainfor each lookup. - If the Console reports an unknown documented terrain, compare the map ID and definition ID letter for letter.
- If visible row or column starts at
0, confirm that the accessible name uses the calculated visible values rather than the stored values.
Assistance 1 — Trace one blocked tile through the renderer
The first map record contains terrain: “blocked”. The lookup returns the blocked definition. The renderer creates one list item, adds map-tile–blocked, names its visible coordinate and terrain, writes X, and appends it.
Assistance 2 — Keep the render order stable
Clear the previous children once before the loop. Create and append one complete tile during each loop pass. Do not create all elements in one loop and append them in a different order.
Assistance 3 — Write renderMap in four phases
Phase 1 clears the old DOM result. Phase 2 loops through fixed map records. Phase 3 validates and transforms one record into one complete list item. Phase 4 appends that item before the loop continues.
Checkpoint: The map definition produces 25 stable DOM tiles
- What now works
- Every coordinate renders once in row-major order, terrain lookup rejects an unknown ID, the Center has the documented coordinate, accessible names use one-based values, and a repeated render still leaves exactly 25 tiles.
- Files changed
game.js- What remains
- Arrange the rendered list as a five-column Grid and make every terrain visible without relying on color.
- Next action
- Open styles.css and add the three map-specific color custom properties beside the existing color properties.
- If it does not work
- Trace the first incorrect tile through its map record, terrain lookup, created list item, attributes, textContent, and append call. Repair the first disagreement before changing CSS.
6. Arrange and identify the terrain
Section titled “6. Arrange and identify the terrain”Add three map-specific color properties inside the existing :root rule:
--color-map-path--color-map-grass--color-map-blocked
The Center can reuse --color-highlight. The blocked tile can reuse --color-surface for its text. Keep the symbols and legend even when the colors appear distinct. The letters are part of the map contract, not a temporary testing aid.
Add .overworld to the existing grouped card rule that styles .trainer-setup and .game-summary. Add .overworld > p to the grouped instruction-text rule that begins with .trainer-setup > p.
Style these relationships:
| Selector | Layout job |
|---|---|
.map-legend |
A wrapping Flexbox row for the four legend entries |
.map-legend li |
A Flexbox row that keeps one symbol beside one label |
.map-key |
A small, centered example of one terrain symbol |
.overworld-map |
The Grid container with five equal flexible column tracks |
.map-tile |
One Grid item with a centered symbol and a visible boundary |
| Terrain modifiers | Matching background roles for both legend keys and rendered tiles |
Write the Grid track list with five 1fr tracks. This repeats the fractional-track syntax already used in the landing-page refinement without adding repeat(), auto-fit, or another Grid feature.
Keep min-inline-size: 0 on .map-tile so each item can shrink inside its track. Use Flexbox inside each tile to center its one symbol. Grid controls the relationship among tiles; Flexbox controls the one-dimensional alignment inside one tile.
Reuse Identify the Grid relationship and Build the single-column Grid first for the container-and-item model. This map keeps five columns at every viewport because the fixed coordinate model must remain visible as a 5 × 5 area. The short symbols and flexible tracks make that contract possible at a narrow width.
Check the layout and meaning
Section titled “Check the layout and meaning”- Use the Grid overlay in DevTools and confirm that
.overworld-maphas five columns and five rows. - Read the tile symbols from left to right and compare each rendered row with the documented table.
- Confirm that the legend explains every symbol without requiring the terrain colors.
- Temporarily disable the background-color declarations. The symbols and legend must still identify every terrain.
- Test near
320px, at a wide viewport, and at200%browser zoom. - Confirm that all five columns remain inside the map section and the page has no horizontal scrolling.
- Press Tab through the page. The map tiles must not become keyboard stops.
If it does not work
Section titled “If it does not work”- If tiles form one column, inspect whether
.overworld-mapreports Grid and whether the five-column track declaration is active. - If a tile widens the page, inspect its minimum inline size, the map gap, and the parent width before reducing content.
- If terrain becomes unclear without color, restore the visible symbols and legend before adjusting the palette.
- If a tile receives focus, remove interactive markup or
tabindex. The tile is information, not a control.
Assistance 1 — Name the two layout relationships
The ordered list is the Grid container, and its 25 direct list items are Grid items. Each list item is also a small Flexbox container whose only job is to center one terrain symbol.
Assistance 2 — Separate shared tile rules from terrain rules
Put size, border, alignment, and type rules on .map-tile. Put only the terrain-specific color on modifier selectors. Apply each modifier color to the matching legend key and map tile in one grouped selector.
Assistance 3 — Style and test in three passes
First make the list a five-column Grid and verify order. Second size and center the tiles without terrain colors. Third add the legend layout and matching terrain colors, then repeat the no-color and narrow-width checks.

Overworld map assistance
Section titled “Overworld map assistance”The earlier assistance supports one decision or test at a time. Use these levels when you need a larger implementation frame. Compare the responsibilities and stable IDs before you copy any part.
Assistance 4 — Use a partial overworld structure
Add this outline between the trainer setup and current-game summary:
<section class="overworld" aria-labelledby="overworld-heading"> <h2 id="overworld-heading">Overworld map</h2> <!-- Add the reading instruction. --> <!-- Add the complete four-entry map legend. --> <ol class="overworld-map" id="overworld-map" aria-label="Five by five overworld map" ></ol></section>Use this JavaScript structure to order the fixed data and renderer:
const terrainDefinitions = [ // Add four records with id, name, and symbol properties.];
const mapTiles = [ // Add 25 row-major records with row, column, and terrain properties.];
const overworldMap = document.querySelector("#overworld-map");
// Add overworldMap to the existing missing-elements guard.
function findTerrainDefinition(terrainId) { // Return the matching fixed definition or null.}
function renderMap() { // Remove the previous rendered children. // Visit every map record. // Validate its terrain lookup. // Create, label, and append one list item.}
// Keep the existing event-listener registration.renderMap();renderGame();Use this CSS structure to separate layout from terrain appearance:
.map-legend { /* Arrange the four legend entries and allow wrapping. */}
.overworld-map { /* Create five equal flexible columns. */}
.map-tile { /* Add the shared item size, border, and symbol alignment. */}
/* Group each map-key modifier with its matching map-tile modifier. */Complete one comment group at a time. Run its local check before you continue.
Assistance 5 — Compare with the complete overworld referenceExample solution
This reference adds only the overworld stage to the completed trainer-setup project. Use it to compare structure, repair one failed checkpoint, or reconstruct a working baseline. Repeat the map counts, trainer route, responsive checks, and Console checks after any copied change.
Add this section between trainer setup and the current-game summary:
<section class="overworld" aria-labelledby="overworld-heading"> <h2 id="overworld-heading">Overworld map</h2> <p> Read the map from the top left to the bottom right. Each tile uses a letter and color to identify its terrain. </p>
<ul class="map-legend" aria-label="Map legend"> <li> <span class="map-key map-key--center">C</span> Pokémon Center </li> <li> <span class="map-key map-key--path">P</span> Path </li> <li> <span class="map-key map-key--grass">G</span> Tall grass </li> <li> <span class="map-key map-key--blocked">X</span> Blocked terrain </li> </ul>
<ol class="overworld-map" id="overworld-map" aria-label="Five by five overworld map" ></ol></section>Add these fixed definitions after starterDefinitions and startingPosition as described in the main path:
const terrainDefinitions = [ { id: "center", name: "Pokémon Center", symbol: "C", }, { id: "path", name: "Path", symbol: "P", }, { id: "grass", name: "Tall grass", symbol: "G", }, { id: "blocked", name: "Blocked terrain", symbol: "X", },];
const mapTiles = [ { row: 0, column: 0, terrain: "blocked" }, { row: 0, column: 1, terrain: "grass" }, { row: 0, column: 2, terrain: "grass" }, { row: 0, column: 3, terrain: "path" }, { row: 0, column: 4, terrain: "blocked" }, { row: 1, column: 0, terrain: "path" }, { row: 1, column: 1, terrain: "path" }, { row: 1, column: 2, terrain: "grass" }, { row: 1, column: 3, terrain: "path" }, { row: 1, column: 4, terrain: "grass" }, { row: 2, column: 0, terrain: "grass" }, { row: 2, column: 1, terrain: "path" }, { row: 2, column: 2, terrain: "center" }, { row: 2, column: 3, terrain: "path" }, { row: 2, column: 4, terrain: "grass" }, { row: 3, column: 0, terrain: "grass" }, { row: 3, column: 1, terrain: "path" }, { row: 3, column: 2, terrain: "path" }, { row: 3, column: 3, terrain: "path" }, { row: 3, column: 4, terrain: "grass" }, { row: 4, column: 0, terrain: "blocked" }, { row: 4, column: 1, terrain: "grass" }, { row: 4, column: 2, terrain: "grass" }, { row: 4, column: 3, terrain: "path" }, { row: 4, column: 4, terrain: "blocked" },];Select the map after the existing gameStatus selection:
const overworldMap = document.querySelector("#overworld-map");Add overworldMap to the existing guard and use the broader message:
if ( !trainerForm || !trainerNameInput || !trainerNameError || !starterChoice || !starterError || !trainerOutput || !starterOutput || !hitPointsOutput || !positionOutput || !encounterOutput || !caughtOutput || !gameStatus || !overworldMap) { throw new Error("Required game elements are missing.");}Add these functions after findStarterDefinition():
function findTerrainDefinition(terrainId) { for (const terrainDefinition of terrainDefinitions) { if (terrainDefinition.id === terrainId) { return terrainDefinition; } }
return null;}
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 tileElement = document.createElement("li"); const visibleRow = mapTile.row + 1; const visibleColumn = mapTile.column + 1;
tileElement.setAttribute( "class", `map-tile map-tile--${mapTile.terrain}`, ); tileElement.setAttribute( "aria-label", `Row ${visibleRow}, column ${visibleColumn}: ${terrainDefinition.name}.`, ); tileElement.textContent = terrainDefinition.symbol;
overworldMap.append(tileElement); }}Keep the trainer listener and both initialization calls at the end of game.js:
trainerForm.addEventListener("submit", handleTrainerSubmit);
renderMap();renderGame();Add the map-specific colors beside the existing color custom properties:
--color-map-path: #f2ddb0;--color-map-grass: #b8d99a;--color-map-blocked: #6d7892;Add .overworld and .overworld > p to the existing grouped trainer rules:
.trainer-setup,.overworld,.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,.overworld > p { margin-block: 0 var(--space-lg); color: var(--color-muted);}Add these focused legend and map rules after that grouped text rule:
.map-legend { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-block: 0 var(--space-lg); padding: 0; list-style: none;}
.map-legend li { display: flex; gap: var(--space-xs); align-items: center;}
.map-key { display: flex; align-items: center; justify-content: center; inline-size: 2rem; min-block-size: 2rem; border: 2px solid var(--color-brand-strong); border-radius: var(--radius-small); font-weight: bold;}
.overworld-map { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr; gap: var(--space-xs); max-inline-size: 32rem; margin: 0; padding: 0; list-style: none;}
.map-tile { display: flex; align-items: center; justify-content: center; min-inline-size: 0; min-block-size: 3.5rem; border: 2px solid var(--color-brand-strong); border-radius: var(--radius-small); font-weight: bold;}
.map-key--center,.map-tile--center { background-color: var(--color-highlight);}
.map-key--path,.map-tile--path { background-color: var(--color-map-path);}
.map-key--grass,.map-tile--grass { background-color: var(--color-map-grass);}
.map-key--blocked,.map-tile--blocked { color: var(--color-surface); background-color: var(--color-map-blocked);}The map renderer reads fixed records rather than the form or current trainer state. replaceChildren() makes the list result repeatable. The terrain lookup keeps display labels and symbols in one fixed definition. The ordered list preserves row-major DOM order, and the five Grid tracks present that same order as coordinates.
7. Verify and record the map checkpoint
Section titled “7. Verify and record the map checkpoint”Save all three files and reload from a clean page state. Run this route:
- Confirm the map renders before trainer setup and contains 25 tiles.
- Compare all five rendered rows with the documented table.
- Confirm the exact terrain counts: 1 Center, 10 path, 10 tall grass, and 4 blocked.
- Inspect the first, middle, and last accessible tile names.
- Run
renderMap()again and confirm the count remains 25. - Submit an empty trainer form and confirm the existing name error, invalid state, focus result, unchanged game state, unchanged map, and clear Console.
- Submit a valid fictional name and each starter choice. Confirm the summary updates while the map records and tiles remain unchanged.
- Reload and confirm trainer state returns to its before-setup values while the fixed map renders again.
- Disable terrain backgrounds and confirm the symbols and legend preserve the complete map meaning.
- Test near
320px, at a wide viewport, and at200%zoom. Confirm no overlap, clipping, or horizontal page scrolling. - Use Tab and Shift + Tab through the page. Confirm that the existing controls retain visible focus and the noninteractive tiles add no stops.
- Validate
game.html, reviewstyles.cssin the VS Code Problems panel, and confirm that the Console reports no error.
Inspect the Git diff. The expected changes are one overworld section in game.html, fixed terrain and map data plus one renderer in game.js, and focused map presentation in styles.css. Remove temporary logs and deliberate test errors.
Use the Level 1 solo-development loop to record the passing map checkpoint separately from trainer setup.
Self-check
Complete these checks against the required result.
- Confirm that game.html keeps the verified trainer interface and adds one labeled overworld section before the current-game summary.
- Confirm that the visible legend defines C, P, G, and X and does not depend on color.
- Confirm that terrainDefinitions contains exactly four fixed records with stable id, name, and symbol properties.
- Confirm that mapTiles contains exactly 25 row-major records and every record has one row, column, and terrain value.
- Confirm that rows and columns cover 0 through 4 once each and the Center record is row 2, column 2.
- Confirm that the rendered map contains 25 tiles: 1 Center, 10 path, 10 tall grass, and 4 blocked.
- Confirm that an unknown terrain ID produces the specific controlled error and the documented IDs render without an error.
- Confirm that a second renderMap call replaces the old tiles and leaves exactly 25 list items.
- Confirm that each tile has a terrain modifier, visible symbol, and one-based accessible row, column, and terrain name.
- Confirm that CSS Grid presents five equal columns in the same order as the map array.
- Confirm that terrain remains understandable when background colors are disabled.
- Confirm that the map has no buttons, links, tabindex values, trainer marker, movement event, or hidden future game logic.
- Repeat the invalid and valid trainer-setup routes and confirm that the previous checkpoint still works.
- Confirm that the page works near 320px and at 200% zoom with no horizontal scrolling.
- Confirm that HTML validation passes, VS Code reports no unresolved CSS author error, and the browser Console is clear.
Next step or safe stopping point
Section titled “Next step or safe stopping point”This is a safe stopping point. The project now has one inspected map definition and one stable visual map. Trainer setup still creates the current position, but no marker displays that position yet.
Continue to Render the trainer position when that article is available. That stage will read state.mapPosition, mark exactly one matching tile, and report the current terrain outside the visual Grid. Do not add movement controls before the marker can move between two temporary test coordinates without leaving an old marker behind.