Generalize tic-tac-toe to an N×N board where M consecutive marks win. The fixed 3×3 board with eight hard-coded lines doesn't scale — for a 9×9 board you can't list every line by hand. Instead, store the board as a flat array and detect a win by scanning from each filled cell along four directions for M in a row.
type Mark = 'X' | 'O' | null;
function getWinner(board: Mark[], size: number, target: number): Mark;
// A self-contained component. No props.
function App(): JSX.Element;
size = 5, target = 4 → place 4 X's in a row/col/diagonal to win
size = 3, target = 3 → classic tic-tac-toe
cell at row r, column c lives at flat index r * size + c
scan directions: → (0,1), ↓ (1,0), ↘ (1,1), ↙ (1,-1)
board[r * size + c]; don't hard-code line lists.M-1 along 4 directions; M in a row wins.size and target come from controls; changing either resets the board.The base game hard-coded eight winning lines — fine for 3×3, hopeless for N×N. Here the board is a flat array indexed by r * size + c, and a win is found by scanning from each filled cell along four directions for M consecutive equal marks. The board size and win length are parameters.
On a 3×3 board there are only 8 lines, so you can list them. On a 9×9 board needing 5-in-a-row there are hundreds — listing them is absurd and size-specific. The scalable approach doesn't enumerate lines at all: for every filled cell, walk outward in the four "forward" directions counting how many same-marks sit in a row; if you ever reach M, that player has won. This one routine works for any size and any target.
board is a flat array of length size * size; the cell at row r, column c is board[r * size + c]. State also holds xIsNext, size, and target. getWinner(board, size, target) loops over every cell; from a filled cell it steps k = 1 … target-1 along each direction vector → ↓ ↘ ↙, stopping at an edge or a different mark; reaching target matches means a win. Changing size or target rebuilds an empty board.
A first attempt tries to generate every line for the current size:
// build all horizontal, vertical, diagonal windows of length M for size N…
// dozens of nested loops, off-by-ones at the edges, rebuilt on every size change
You can generate all length-M windows, but it's a lot of fiddly index math that's easy to get wrong at the borders, and it has to regenerate whenever size/target change. Scanning from each cell is simpler: the bounds check (nr, nc in range) handles edges naturally, and nothing needs precomputing.
import { useState } from 'react';
import './styles.css';
type Mark = 'X' | 'O' | null;
const DIRS = [
[0, 1], // →
[1, 0], // ↓
[1, 1], // ↘
[1, -1], // ↙
];
function getWinner(board: Mark[], size: number, target: number): Mark {
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
const mark = board[r * size + c];
if (!mark) continue;
for (const [dr, dc] of DIRS) {
let k = 1;
while (k < target) {
const nr = r + dr * k;
const nc = c + dc * k;
if (nr < 0 || nr >= size || nc < 0 || nc >= size || board[nr * size + nc] !== mark) {
break;
}
k++;
}
if (k === target) return mark;
}
}
}
return null;
}
export default function App() {
const [size, setSize] = useState(5);
const [target, setTarget] = useState(4);
const [board, setBoard] = useState<Mark[]>(() => Array(size * size).fill(null));
const [xIsNext, setXIsNext] = useState(true);
const winner = getWinner(board, size, target);
const isDraw = !winner && board.every((cell) => cell !== null);
const status = winner ? `${winner} wins!` : isDraw ? 'Draw!' : `${xIsNext ? 'X' : 'O'}'s turn`;
function play(i: number) {
if (board[i] || winner) return;
const next = board.slice();
next[i] = xIsNext ? 'X' : 'O';
setBoard(next);
setXIsNext(!xIsNext);
}
function reset(n: number, m: number) {
setSize(n);
setTarget(m);
setBoard(Array(n * n).fill(null));
setXIsNext(true);
}
return (
<main className="container">
<h1>Tic-tac-toe II</h1>
<div className="controls">
<label>
Board
<select value={size} onChange={(e) => reset(Number(e.target.value), target)}>
<option value={3}>3 × 3</option>
<option value={5}>5 × 5</option>
<option value={6}>6 × 6</option>
</select>
</label>
<label>
Win
<select value={target} onChange={(e) => reset(size, Number(e.target.value))}>
<option value={3}>3 in a row</option>
<option value={4}>4 in a row</option>
<option value={5}>5 in a row</option>
</select>
</label>
</div>
<p className="status">{status}</p>
<div className="board" style={{ gridTemplateColumns: `repeat(${size}, 1fr)` }}>
{board.map((cell, i) => (
<button
key={i}
className={`square${cell === 'X' ? ' x' : cell === 'O' ? ' o' : ''}`}
onClick={() => play(i)}
disabled={!!cell || !!winner}
>
{cell}
</button>
))}
</div>
</main>
);
}
getWinner is the generalization: it never lists lines — it scans DIRS from each filled cell, and the in-range check makes edges safe. The board is a flat array; r * size + c converts coordinates to an index and back. play is the base game's guarded immutable write. reset(n, m) is the one place size/target change, and it always rebuilds an empty board so a stale board never outlives a size change. The grid's columns come from an inline repeat(${size}, 1fr).
size = 5, target = 4, empty board.
(0,0), (1,1), (2,2). After the 3rd, getWinner scans: from (0,0) along ↘, it finds X at (1,1), (2,2) then a blank → k reaches 3, not 4. No winner yet.(3,3). Now from (0,0) along ↘: (1,1), (2,2), (3,3) all X → k hits target (4) → returns 'X'. Status "X wins!"; all squares disable.reset(5, 5) clears the board; now you need five in a row.reset(3, 3) → it's classic tic-tac-toe, same code.size-specific. Fix: scan from each cell along 4 directions.undefined/wraps. Fix: check nr/nc in range before reading.reset.board.slice().[value] on Angular <select>. Won't show the selection; use [selected] on options (see the Angular variant).getWinner and style them.This version makes play and configuration changes explicit reducer actions. Winner and draw remain derived so the rendered interface stays identical.
import { useReducer } from 'react';
import './styles.css';
type Mark = 'X' | 'O' | null;
type State = { size: number; target: number; board: Mark[]; xIsNext: boolean };
type Action =
| { type: 'play'; index: number }
| { type: 'size'; value: number }
| { type: 'target'; value: number };
const DIRS = [[0, 1], [1, 0], [1, 1], [1, -1]];
function getWinner(board: Mark[], size: number, target: number): Mark {
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
const mark = board[row * size + col];
if (!mark) continue;
for (const [dr, dc] of DIRS) {
let count = 1;
while (count < target) {
const r = row + dr * count;
const c = col + dc * count;
if (r < 0 || r >= size || c < 0 || c >= size || board[r * size + c] !== mark) break;
count += 1;
}
if (count === target) return mark;
}
}
}
return null;
}
const initialState: State = {
size: 5,
target: 4,
board: Array(25).fill(null),
xIsNext: true,
};
function reset(size: number, target: number): State {
return { size, target, board: Array(size * size).fill(null), xIsNext: true };
}
function reducer(state: State, action: Action): State {
if (action.type === 'size') return reset(action.value, state.target);
if (action.type === 'target') return reset(state.size, action.value);
if (state.board[action.index] || getWinner(state.board, state.size, state.target)) return state;
const board = state.board.slice();
board[action.index] = state.xIsNext ? 'X' : 'O';
return { ...state, board, xIsNext: !state.xIsNext };
}
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const winner = getWinner(state.board, state.size, state.target);
const draw = !winner && state.board.every(Boolean);
const status = winner ? `${winner} wins!` : draw ? 'Draw!' : `${state.xIsNext ? 'X' : 'O'}'s turn`;
return (
<main className="container">
<h1>Tic-tac-toe II</h1>
<div className="controls">
<label>
Board
<select value={state.size} onChange={(event) => dispatch({ type: 'size', value: Number(event.target.value) })}>
<option value={3}>3 × 3</option>
<option value={5}>5 × 5</option>
<option value={6}>6 × 6</option>
</select>
</label>
<label>
Win
<select value={state.target} onChange={(event) => dispatch({ type: 'target', value: Number(event.target.value) })}>
<option value={3}>3 in a row</option>
<option value={4}>4 in a row</option>
<option value={5}>5 in a row</option>
</select>
</label>
</div>
<p className="status">{status}</p>
<div className="board" style={{ gridTemplateColumns: `repeat(${state.size}, 1fr)` }}>
{state.board.map((cell, index) => (
<button
key={index}
className={`square${cell === 'X' ? ' x' : cell === 'O' ? ' o' : ''}`}
disabled={!!cell || !!winner}
onClick={() => dispatch({ type: 'play', index })}
>
{cell}
</button>
))}
</div>
</main>
);
}This version checks only the row, column, and diagonals that pass through the latest move. Opposite direction counts are combined around the new mark.
import { useState } from 'react';
import './styles.css';
type Mark = 'X' | 'O' | null;
const AXES = [[0, 1], [1, 0], [1, 1], [1, -1]];
function winnerAt(board: Mark[], size: number, target: number, index: number): Mark {
const mark = board[index];
if (!mark) return null;
const row = Math.floor(index / size);
const col = index % size;
const count = (dr: number, dc: number) => {
let total = 0;
let r = row + dr;
let c = col + dc;
while (r >= 0 && r < size && c >= 0 && c < size && board[r * size + c] === mark) {
total += 1;
r += dr;
c += dc;
}
return total;
};
return AXES.some(([dr, dc]) => 1 + count(dr, dc) + count(-dr, -dc) >= target) ? mark : null;
}
export default function App() {
const [size, setSize] = useState(5);
const [target, setTarget] = useState(4);
const [board, setBoard] = useState<Mark[]>(() => Array(25).fill(null));
const [xIsNext, setXIsNext] = useState(true);
const [winner, setWinner] = useState<Mark>(null);
const draw = !winner && board.every(Boolean);
const status = winner ? `${winner} wins!` : draw ? 'Draw!' : `${xIsNext ? 'X' : 'O'}'s turn`;
function reset(nextSize: number, nextTarget: number) {
setSize(nextSize);
setTarget(nextTarget);
setBoard(Array(nextSize * nextSize).fill(null));
setXIsNext(true);
setWinner(null);
}
function play(index: number) {
if (board[index] || winner) return;
const next = board.slice();
next[index] = xIsNext ? 'X' : 'O';
setBoard(next);
setWinner(winnerAt(next, size, target, index));
setXIsNext(!xIsNext);
}
return (
<main className="container">
<h1>Tic-tac-toe II</h1>
<div className="controls">
<label>
Board
<select value={size} onChange={(event) => reset(Number(event.target.value), target)}>
<option value={3}>3 × 3</option>
<option value={5}>5 × 5</option>
<option value={6}>6 × 6</option>
</select>
</label>
<label>
Win
<select value={target} onChange={(event) => reset(size, Number(event.target.value))}>
<option value={3}>3 in a row</option>
<option value={4}>4 in a row</option>
<option value={5}>5 in a row</option>
</select>
</label>
</div>
<p className="status">{status}</p>
<div className="board" style={{ gridTemplateColumns: `repeat(${size}, 1fr)` }}>
{board.map((cell, index) => (
<button
key={index}
className={`square${cell === 'X' ? ' x' : cell === 'O' ? ' o' : ''}`}
disabled={!!cell || !!winner}
onClick={() => play(index)}
>
{cell}
</button>
))}
</div>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Generalize tic-tac-toe to an N×N board where M consecutive marks win. The fixed 3×3 board with eight hard-coded lines doesn't scale — for a 9×9 board you can't list every line by hand. Instead, store the board as a flat array and detect a win by scanning from each filled cell along four directions for M in a row.
type Mark = 'X' | 'O' | null;
function getWinner(board: Mark[], size: number, target: number): Mark;
// A self-contained component. No props.
function App(): JSX.Element;
size = 5, target = 4 → place 4 X's in a row/col/diagonal to win
size = 3, target = 3 → classic tic-tac-toe
cell at row r, column c lives at flat index r * size + c
scan directions: → (0,1), ↓ (1,0), ↘ (1,1), ↙ (1,-1)
board[r * size + c]; don't hard-code line lists.M-1 along 4 directions; M in a row wins.size and target come from controls; changing either resets the board.The base game hard-coded eight winning lines — fine for 3×3, hopeless for N×N. Here the board is a flat array indexed by r * size + c, and a win is found by scanning from each filled cell along four directions for M consecutive equal marks. The board size and win length are parameters.
On a 3×3 board there are only 8 lines, so you can list them. On a 9×9 board needing 5-in-a-row there are hundreds — listing them is absurd and size-specific. The scalable approach doesn't enumerate lines at all: for every filled cell, walk outward in the four "forward" directions counting how many same-marks sit in a row; if you ever reach M, that player has won. This one routine works for any size and any target.
board is a flat array of length size * size; the cell at row r, column c is board[r * size + c]. State also holds xIsNext, size, and target. getWinner(board, size, target) loops over every cell; from a filled cell it steps k = 1 … target-1 along each direction vector → ↓ ↘ ↙, stopping at an edge or a different mark; reaching target matches means a win. Changing size or target rebuilds an empty board.
A first attempt tries to generate every line for the current size:
// build all horizontal, vertical, diagonal windows of length M for size N…
// dozens of nested loops, off-by-ones at the edges, rebuilt on every size change
You can generate all length-M windows, but it's a lot of fiddly index math that's easy to get wrong at the borders, and it has to regenerate whenever size/target change. Scanning from each cell is simpler: the bounds check (nr, nc in range) handles edges naturally, and nothing needs precomputing.
import { useState } from 'react';
import './styles.css';
type Mark = 'X' | 'O' | null;
const DIRS = [
[0, 1], // →
[1, 0], // ↓
[1, 1], // ↘
[1, -1], // ↙
];
function getWinner(board: Mark[], size: number, target: number): Mark {
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
const mark = board[r * size + c];
if (!mark) continue;
for (const [dr, dc] of DIRS) {
let k = 1;
while (k < target) {
const nr = r + dr * k;
const nc = c + dc * k;
if (nr < 0 || nr >= size || nc < 0 || nc >= size || board[nr * size + nc] !== mark) {
break;
}
k++;
}
if (k === target) return mark;
}
}
}
return null;
}
export default function App() {
const [size, setSize] = useState(5);
const [target, setTarget] = useState(4);
const [board, setBoard] = useState<Mark[]>(() => Array(size * size).fill(null));
const [xIsNext, setXIsNext] = useState(true);
const winner = getWinner(board, size, target);
const isDraw = !winner && board.every((cell) => cell !== null);
const status = winner ? `${winner} wins!` : isDraw ? 'Draw!' : `${xIsNext ? 'X' : 'O'}'s turn`;
function play(i: number) {
if (board[i] || winner) return;
const next = board.slice();
next[i] = xIsNext ? 'X' : 'O';
setBoard(next);
setXIsNext(!xIsNext);
}
function reset(n: number, m: number) {
setSize(n);
setTarget(m);
setBoard(Array(n * n).fill(null));
setXIsNext(true);
}
return (
<main className="container">
<h1>Tic-tac-toe II</h1>
<div className="controls">
<label>
Board
<select value={size} onChange={(e) => reset(Number(e.target.value), target)}>
<option value={3}>3 × 3</option>
<option value={5}>5 × 5</option>
<option value={6}>6 × 6</option>
</select>
</label>
<label>
Win
<select value={target} onChange={(e) => reset(size, Number(e.target.value))}>
<option value={3}>3 in a row</option>
<option value={4}>4 in a row</option>
<option value={5}>5 in a row</option>
</select>
</label>
</div>
<p className="status">{status}</p>
<div className="board" style={{ gridTemplateColumns: `repeat(${size}, 1fr)` }}>
{board.map((cell, i) => (
<button
key={i}
className={`square${cell === 'X' ? ' x' : cell === 'O' ? ' o' : ''}`}
onClick={() => play(i)}
disabled={!!cell || !!winner}
>
{cell}
</button>
))}
</div>
</main>
);
}
getWinner is the generalization: it never lists lines — it scans DIRS from each filled cell, and the in-range check makes edges safe. The board is a flat array; r * size + c converts coordinates to an index and back. play is the base game's guarded immutable write. reset(n, m) is the one place size/target change, and it always rebuilds an empty board so a stale board never outlives a size change. The grid's columns come from an inline repeat(${size}, 1fr).
size = 5, target = 4, empty board.
(0,0), (1,1), (2,2). After the 3rd, getWinner scans: from (0,0) along ↘, it finds X at (1,1), (2,2) then a blank → k reaches 3, not 4. No winner yet.(3,3). Now from (0,0) along ↘: (1,1), (2,2), (3,3) all X → k hits target (4) → returns 'X'. Status "X wins!"; all squares disable.reset(5, 5) clears the board; now you need five in a row.reset(3, 3) → it's classic tic-tac-toe, same code.size-specific. Fix: scan from each cell along 4 directions.undefined/wraps. Fix: check nr/nc in range before reading.reset.board.slice().[value] on Angular <select>. Won't show the selection; use [selected] on options (see the Angular variant).getWinner and style them.This version makes play and configuration changes explicit reducer actions. Winner and draw remain derived so the rendered interface stays identical.
import { useReducer } from 'react';
import './styles.css';
type Mark = 'X' | 'O' | null;
type State = { size: number; target: number; board: Mark[]; xIsNext: boolean };
type Action =
| { type: 'play'; index: number }
| { type: 'size'; value: number }
| { type: 'target'; value: number };
const DIRS = [[0, 1], [1, 0], [1, 1], [1, -1]];
function getWinner(board: Mark[], size: number, target: number): Mark {
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
const mark = board[row * size + col];
if (!mark) continue;
for (const [dr, dc] of DIRS) {
let count = 1;
while (count < target) {
const r = row + dr * count;
const c = col + dc * count;
if (r < 0 || r >= size || c < 0 || c >= size || board[r * size + c] !== mark) break;
count += 1;
}
if (count === target) return mark;
}
}
}
return null;
}
const initialState: State = {
size: 5,
target: 4,
board: Array(25).fill(null),
xIsNext: true,
};
function reset(size: number, target: number): State {
return { size, target, board: Array(size * size).fill(null), xIsNext: true };
}
function reducer(state: State, action: Action): State {
if (action.type === 'size') return reset(action.value, state.target);
if (action.type === 'target') return reset(state.size, action.value);
if (state.board[action.index] || getWinner(state.board, state.size, state.target)) return state;
const board = state.board.slice();
board[action.index] = state.xIsNext ? 'X' : 'O';
return { ...state, board, xIsNext: !state.xIsNext };
}
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const winner = getWinner(state.board, state.size, state.target);
const draw = !winner && state.board.every(Boolean);
const status = winner ? `${winner} wins!` : draw ? 'Draw!' : `${state.xIsNext ? 'X' : 'O'}'s turn`;
return (
<main className="container">
<h1>Tic-tac-toe II</h1>
<div className="controls">
<label>
Board
<select value={state.size} onChange={(event) => dispatch({ type: 'size', value: Number(event.target.value) })}>
<option value={3}>3 × 3</option>
<option value={5}>5 × 5</option>
<option value={6}>6 × 6</option>
</select>
</label>
<label>
Win
<select value={state.target} onChange={(event) => dispatch({ type: 'target', value: Number(event.target.value) })}>
<option value={3}>3 in a row</option>
<option value={4}>4 in a row</option>
<option value={5}>5 in a row</option>
</select>
</label>
</div>
<p className="status">{status}</p>
<div className="board" style={{ gridTemplateColumns: `repeat(${state.size}, 1fr)` }}>
{state.board.map((cell, index) => (
<button
key={index}
className={`square${cell === 'X' ? ' x' : cell === 'O' ? ' o' : ''}`}
disabled={!!cell || !!winner}
onClick={() => dispatch({ type: 'play', index })}
>
{cell}
</button>
))}
</div>
</main>
);
}This version checks only the row, column, and diagonals that pass through the latest move. Opposite direction counts are combined around the new mark.
import { useState } from 'react';
import './styles.css';
type Mark = 'X' | 'O' | null;
const AXES = [[0, 1], [1, 0], [1, 1], [1, -1]];
function winnerAt(board: Mark[], size: number, target: number, index: number): Mark {
const mark = board[index];
if (!mark) return null;
const row = Math.floor(index / size);
const col = index % size;
const count = (dr: number, dc: number) => {
let total = 0;
let r = row + dr;
let c = col + dc;
while (r >= 0 && r < size && c >= 0 && c < size && board[r * size + c] === mark) {
total += 1;
r += dr;
c += dc;
}
return total;
};
return AXES.some(([dr, dc]) => 1 + count(dr, dc) + count(-dr, -dc) >= target) ? mark : null;
}
export default function App() {
const [size, setSize] = useState(5);
const [target, setTarget] = useState(4);
const [board, setBoard] = useState<Mark[]>(() => Array(25).fill(null));
const [xIsNext, setXIsNext] = useState(true);
const [winner, setWinner] = useState<Mark>(null);
const draw = !winner && board.every(Boolean);
const status = winner ? `${winner} wins!` : draw ? 'Draw!' : `${xIsNext ? 'X' : 'O'}'s turn`;
function reset(nextSize: number, nextTarget: number) {
setSize(nextSize);
setTarget(nextTarget);
setBoard(Array(nextSize * nextSize).fill(null));
setXIsNext(true);
setWinner(null);
}
function play(index: number) {
if (board[index] || winner) return;
const next = board.slice();
next[index] = xIsNext ? 'X' : 'O';
setBoard(next);
setWinner(winnerAt(next, size, target, index));
setXIsNext(!xIsNext);
}
return (
<main className="container">
<h1>Tic-tac-toe II</h1>
<div className="controls">
<label>
Board
<select value={size} onChange={(event) => reset(Number(event.target.value), target)}>
<option value={3}>3 × 3</option>
<option value={5}>5 × 5</option>
<option value={6}>6 × 6</option>
</select>
</label>
<label>
Win
<select value={target} onChange={(event) => reset(size, Number(event.target.value))}>
<option value={3}>3 in a row</option>
<option value={4}>4 in a row</option>
<option value={5}>5 in a row</option>
</select>
</label>
</div>
<p className="status">{status}</p>
<div className="board" style={{ gridTemplateColumns: `repeat(${size}, 1fr)` }}>
{board.map((cell, index) => (
<button
key={index}
className={`square${cell === 'X' ? ' x' : cell === 'O' ? ' o' : ''}`}
disabled={!!cell || !!winner}
onClick={() => play(index)}
>
{cell}
</button>
))}
</div>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.