Practice 5 - Hangman Completed & Pixel Art Editor
Introduction
This session has two parts. First we finish the Hangman game by adding the SVG drawing figure and a proper lose condition. Then we start a brand-new project: a Pixel Art Editor.
New concepts covered today:
Array.slice()— extracting a portion of an array without modifying it- SVG as data — storing drawing instructions in a JavaScript array
Array.filter()for counting — deriving the mistake count from stateArray.some()andArray.every()— readable alternatives to manual loops- A single state object — grouping all application data in one place
- 2D arrays — a grid stored as rows of columns
data-*attributes — embedding coordinates in HTML for click handling
Part 1 — Finishing Hangman
1. Array.slice() — Extracting a Portion of an Array
const arr = [1, 2, 3, 4, 5];
arr.slice(1, 3); // [2, 3] — index 1 inclusive to 3 exclusive
arr.slice(0, 2); // [1, 2]
arr.slice(2); // [3, 4, 5] — from index 2 to end
slice(start, end):
- Returns a new array — the original is never modified
startis inclusive,endis exclusive- If
endis omitted, it runs to the end of the array
This is the key to progressive SVG drawing: given N mistakes, show the first N shapes.
2. SVG as Data — visualElements
The hangman figure is a sequence of SVG shapes. Instead of hard-coding them in HTML, we store them as an array of strings in JavaScript:
const visualElements = [
`<line x1="0" y1="99%" x2="100%" y2="99%" />`, // floor
`<line x1="20%" y1="99%" x2="20%" y2="5%" />`, // pole
`<line x1="20%" y1="5%" x2="60%" y2="5%" />`, // top bar
`<line x1="60%" y1="5%" x2="60%" y2="20%" />`, // rope
`<circle cx="60%" cy="30%" r="10%" />`, // head
`<line x1="60%" y1="30%" x2="60%" y2="70%" />`, // body
`<line x1="40%" y1="50%" x2="80%" y2="50%" />`, // arms
`<line x1="60%" y1="70%" x2="50%" y2="90%" />`, // left leg
`<line x1="60%" y1="70%" x2="70%" y2="90%" />` // right leg
];
The HTML SVG element gets an id so we can target it from JavaScript:
<svg width="200px" height="200px" stroke="black" id="littleMan"></svg>
3. Counting Mistakes
A “mistake” is a guessed letter that does not appear in targetWord. We derive this count from state with filter:
function mistakes() {
return guessedLetters.filter(
letter => !targetWord.toLowerCase().includes(letter.toLowerCase())
).length;
}
We never store a separate wrongGuesses variable. The count is always computed fresh from guessedLetters and targetWord, so it can never get out of sync with reality.
4. Drawing the Figure — renderLittleMan()
Now slice() and mistakes() snap together:
function renderLittleMan() {
const svg = document.querySelector("#littleMan");
svg.innerHTML = visualElements.slice(0, mistakes()).join("");
}
visualElements.slice(0, mistakes()) returns the first N elements, where N is the current wrong-guess count. Each re-render reveals exactly as many body parts as wrong guesses made so far.
This function joins the render pipeline:
function renderState() {
renderButtons();
renderWord();
renderLittleMan(); // ← new this session
checkEndGame();
}
5. Win and Lose — checkEndGame()
Previously the function only checked for a win. Now it handles both outcomes:
function checkEndGame() {
// Lose condition — checked first
if (mistakes() > visualElements.length) {
gameOver = true;
document.querySelector("#result").innerText =
`You died. The word was: ${targetWord}.`;
document.querySelector("#controls").style.display = "block";
return; // stop here — do not check win
}
// Win condition
if (targetWord.toUpperCase().split("").every(
letter => guessedLetters.includes(letter)
)) {
document.querySelector("#result").innerText =
`Congratulations, you have guessed the word!`;
document.querySelector("#controls").style.display = "block";
gameOver = true;
}
}
The return after the lose branch is an early return: once we know the game is lost there is nothing more to check. Without it, the win condition would also run and could display a confusing second message.
Early returns keep code flat. Instead of nesting the win check inside an else, we exit as soon as we have our answer.
6. some() and every()
We used every() in the win check. Meet its sibling some():
const numbers = [1, 2, 3, 4, 5];
// every — is the condition true for ALL elements?
numbers.every(n => n % 2 === 0); // false — 1, 3, 5 are odd
numbers.every(n => n < 10); // true — all are below 10
// some — is the condition true for AT LEAST ONE element?
numbers.some(n => n % 2 === 0); // true — 2 and 4 are even
numbers.some(n => n < 0); // false — no negatives
| Method | Returns true when… | Short-circuits on |
|---|---|---|
every() | all elements pass | first failure |
some() | at least one passes | first success |
Both stop iterating as soon as they have enough information.
In the win check we use every() because the word is guessed only when all of its letters are in guessedLetters.
Complete Hangman Data Flow
Start button click
startGame() → reset state → renderState()
Each letter click
handleButtonClick() → push to guessedLetters → renderState()
├── renderButtons() — alphabet, guessed letters disabled
├── renderWord() — _ or real letter per character
├── renderLittleMan() — first N SVG shapes (N = mistakes())
└── checkEndGame() — lose if mistakes > 9, win if every letter guessed
Part 2 — Pixel Art Editor
Project Structure
practice-05/
├── index.html # Three-column layout
├── style.css # Table and layout styles
└── main.js # State object and render logic
HTML Overview
The page has three side-by-side panels:
<div id="app">
<!-- Panel 1: grid editor -->
<div id="editor-section">
<div id="grid-controls">
<label>Width: <input type="number" id="grid-width" value="10" /></label>
<label>Height: <input type="number" id="grid-height" value="10" /></label>
<button id="generate-btn">Generate Grid</button>
</div>
<div id="grid-container"></div>
<button id="save-btn">Save</button>
</div>
<!-- Panel 2: colour picker -->
<div id="color-section">
<h2>Color</h2>
<div id="current-color-display"></div>
<input type="color" id="color-picker" value="#ff0000" />
</div>
<!-- Panel 3: saved arts list -->
<div id="list-section">
<h2>Pixel Arts</h2>
<button id="new-btn">+ New</button>
<div id="pixel-art-list"></div>
</div>
</div>
The three-column layout uses a single Flexbox rule:
#app {
display: flex;
gap: 24px;
align-items: flex-start;
}
align-items: flex-start keeps all panels top-aligned. Without it, a tall grid would stretch the colour and list panels to match its height.
1. The State Object
Previous projects used separate let variables for each piece of data. Here everything lives in one object:
let state = {
gridWidth: 10,
gridHeight: 10,
pixels: [], // 2D array: pixels[row][col] = "#rrggbb" or null
currentColor: '#ff0000',
};
Why a state object?
| Separate variables | One state object |
|---|---|
| Scattered across the file | All data in one place |
| Easy to forget one when resetting | state = { ... } resets everything |
| Hard to save/load | JSON.stringify(state) captures everything |
As an app grows, a single state object is much easier to reason about than a collection of loose variables.
2. The 2D Pixel Array
The grid is stored as an array of rows, where each row is an array of colour strings (or null for unpainted cells):
pixels = [
[ "#ff0000", null, "#0000ff" ], // row 0
[ null, "#ff0000", null ], // row 1
[ "#0000ff", null, null ], // row 2
]
pixels[row][col] gives the colour at that cell.
When we generate a fresh grid:
state.pixels = Array.from({ length: state.gridHeight }, () =>
Array.from({ length: state.gridWidth }, () => null)
);
Array.from({ length: N }, fn) creates an array of N elements by calling fn for each slot. The outer call builds one row per grid height; the inner call fills each row with null values.
3. Rendering the Grid — renderGrid()
function renderGrid() {
const rows = state.pixels.map((row, rowIndex) => {
return `<tr>` +
row.map((color, colIndex) => {
return `
<td
data-row="${rowIndex}"
data-col="${colIndex}"
style="${color ? 'background-color:' + color : ''}"
></td>
`;
}).join("") +
`</tr>`;
}).join("");
document.querySelector("#grid-container").innerHTML = `
<table class="edit">
<tbody>${rows}</tbody>
</table>
`;
}
Outer map — rows
state.pixels.map((row, rowIndex) => ...) iterates over each row. The second argument rowIndex is the row’s index inside the 2D array — we store it in data-row.
Inner map — cells
row.map((color, colIndex) => ...) iterates over each cell in that row. colIndex goes into data-col.
Inline style from a ternary
style="${color ? 'background-color:' + color : ''}"
If color is a truthy hex string, inject a background rule. If it is null, the attribute is empty and the cell shows the default CSS background.
4. data-* Attributes
<td data-row="2" data-col="5" ...></td>
data-* attributes let you attach arbitrary data to any HTML element. JavaScript reads them through element.dataset:
element.dataset.row // "2" (always a string)
element.dataset.col // "5"
When the user clicks a cell, event.target.dataset.row and event.target.dataset.col tell us exactly which cell to paint — no DOM traversal needed.
5. CSS — Two Table Variants
Two table class variants handle editing canvas and thumbnails:
/* Editing canvas — large, visible grid lines */
table.edit td {
width: 20px;
height: 20px;
border: 1px solid lightgray;
padding: 0;
}
/* Saved-art thumbnail — tiny, no borders */
table.preview td {
width: 3px;
height: 3px;
border: none;
padding: 0;
}
The same 2D array renders into either table. The editor uses table.edit; saved-art thumbnails in the list panel will use table.preview.
6. Render Entry Point
function renderAll() {
renderGrid();
}
renderAll(); // initial render on page load
Same pattern as Hangman: one entry point, specialised render functions below. We will add renderColorPicker() and renderList() here as the project grows.
What Comes Next
The scaffold is in place. The remaining features follow directly from the patterns above:
Painting cells — click event on #grid-container, delegation with event.target.closest("td"), read dataset.row / dataset.col, update state.pixels[row][col], call renderAll().
Colour picker — input event on #color-picker, update state.currentColor, refresh #current-color-display.
Generate grid — click on #generate-btn, read width/height inputs into state, rebuild state.pixels as a fresh null grid, call renderAll().
Save — push a deep copy of state.pixels into a savedArts array, re-render the list panel with thumbnails.
Summary
| Concept | Where we used it |
|---|---|
slice(start, end) | Show first N SVG shapes (N = mistake count) |
| SVG as a data array | visualElements — inject with innerHTML |
filter().length | Count wrong guesses without a separate variable |
| Early return | Exit checkEndGame() as soon as lose is confirmed |
every() | Win: every letter of the word must be guessed |
some() | At least one element satisfies a condition |
| State object | let state = { ... } — single source of truth |
| 2D array | pixels[row][col] grid |
Array.from({ length }, fn) | Generate a fresh N×M grid |
map(item, index) | Track row/column position while building HTML |
data-row / data-col | Encode grid coordinates in the DOM |
element.dataset | Read data-* values in JavaScript |
| CSS table variants | table.edit (20px) vs table.preview (3px) |