Overview
The Tic-Tac-Toe game showcases:
Game state management
Win condition checking
Reducer pattern for state updates
Conditional rendering
Modular code organization
Live Demo View the complete source code on GitHub
What You’ll Learn
Game Logic Implementing win detection algorithms
Reducer Pattern Using reducers for state transitions
Modular Code Separating logic from UI
Grid Rendering Rendering 2D arrays efficiently
Complete Code
Game Logic (game.js)
export function makeInitialState () {
return {
board: [
[ null , null , null ],
[ null , null , null ],
[ null , null , null ],
],
player: "X" ,
draw: false ,
winner: null ,
};
}
export function markReducer ( state , { row , col }) {
if ( row > 3 || row < 0 || col > 3 || col < 0 ) {
throw new Error ( "Invalid move" );
}
if ( state . board [ row ][ col ]) {
throw new Error ( "Invalid move" );
}
const newBoard = [
[ ... state . board [ 0 ]],
[ ... state . board [ 1 ]],
[ ... state . board [ 2 ]],
];
newBoard [ row ][ col ] = state . player ;
const newPlayer = state . player === "X" ? "O" : "X" ;
const winner = checkWinner ( newBoard , state . player );
const draw = ! winner && newBoard . every (( row ) => row . every (( cell ) => cell ));
return {
board: newBoard ,
player: newPlayer ,
draw ,
winner ,
};
}
const checkWinner = ( board , player ) => {
// Check rows
for ( let i = 0 ; i < 3 ; i ++ ) {
if ( checkRow ( board , i , player )) {
return player ;
}
}
// Check columns
for ( let i = 0 ; i < 3 ; i ++ ) {
if ( checkColumn ( board , i , player )) {
return player ;
}
}
// Check diagonals
if ( checkMainDiagonal ( board , player )) {
return player ;
}
if ( checkSecondaryDiagonal ( board , player )) {
return player ;
}
return null ;
};
const checkRow = ( board , idx , player ) => {
const row = board [ idx ];
return row . every (( cell ) => cell === player );
};
const checkColumn = ( board , idx , player ) => {
const column = [ board [ 0 ][ idx ], board [ 1 ][ idx ], board [ 2 ][ idx ]];
return column . every (( cell ) => cell === player );
};
const checkMainDiagonal = ( board , player ) => {
const diagonal = [ board [ 0 ][ 0 ], board [ 1 ][ 1 ], board [ 2 ][ 2 ]];
return diagonal . every (( cell ) => cell === player );
};
const checkSecondaryDiagonal = ( board , player ) => {
const diagonal = [ board [ 0 ][ 2 ], board [ 1 ][ 1 ], board [ 2 ][ 0 ]];
return diagonal . every (( cell ) => cell === player );
};
Component (app.js)
import {
Component ,
h ,
hFragment ,
} from "@glyphui/runtime" ;
import { makeInitialState , markReducer } from "./game.js" ;
class TicTacToeApp extends Component {
constructor () {
super ({}, {
initialState: makeInitialState ()
});
}
mark ( position ) {
const { row , col } = position ;
if ( row > 3 || row < 0 || col > 3 || col < 0 ) {
return ; // Invalid move
}
if ( this . state . board [ row ][ col ]) {
return ; // Cell already marked
}
this . setState ( markReducer ( this . state , position ));
}
render ( props , state ) {
return hFragment ([
this . renderHeader ( state ),
this . renderBoard ( state )
]);
}
renderHeader ( state ) {
if ( state . winner ) {
return h ( "h3" , { class: "win-title" }, [
`Player ${ state . winner } wins!` ,
]);
}
if ( state . draw ) {
return h ( "h3" , { class: "draw-title" }, [ `It's a draw!` ]);
}
return h ( "h3" , {}, [ `It's ${ state . player } 's turn!` ]);
}
renderBoard ( state ) {
const freezeBoard = state . winner || state . draw ;
return h ( "table" , { class: freezeBoard ? "frozen" : "" }, [
h (
"tbody" ,
{},
state . board . map (( row , i ) => this . renderRow ( row , i ))
),
]);
}
renderRow ( row , i ) {
return h (
"tr" ,
{},
row . map (( cell , j ) => this . renderCell ( cell , i , j ))
);
}
renderCell ( cell , i , j ) {
const mark = cell
? h ( "span" , { class: "cell-text" }, [ cell ])
: h (
"div" ,
{
class: "cell" ,
on: { click : () => this . mark ({ row: i , col: j }) },
},
[]
);
return h ( "td" , {}, [ mark ]);
}
}
const game = new TicTacToeApp ();
game . mount ( document . body );
Key Concepts
1. Reducer Pattern
A reducer is a pure function that takes state and an action, returning new state:
export function markReducer ( state , { row , col }) {
// Create new board (immutability)
const newBoard = [
[ ... state . board [ 0 ]],
[ ... state . board [ 1 ]],
[ ... state . board [ 2 ]],
];
newBoard [ row ][ col ] = state . player ;
// Calculate derived state
const winner = checkWinner ( newBoard , state . player );
const draw = ! winner && newBoard . every ( row => row . every ( cell => cell ));
// Return new state object
return {
board: newBoard ,
player: newPlayer ,
draw ,
winner ,
};
}
Reducers should be pure functions with no side effects, always returning new state objects.
2. Win Detection
The game checks rows, columns, and diagonals:
const checkWinner = ( board , player ) => {
// Check all rows
for ( let i = 0 ; i < 3 ; i ++ ) {
if ( checkRow ( board , i , player )) return player ;
}
// Check all columns
for ( let i = 0 ; i < 3 ; i ++ ) {
if ( checkColumn ( board , i , player )) return player ;
}
// Check both diagonals
if ( checkMainDiagonal ( board , player )) return player ;
if ( checkSecondaryDiagonal ( board , player )) return player ;
return null ;
};
3. Immutable Updates
Always create new arrays/objects instead of mutating:
// ❌ Wrong - mutates original
state . board [ row ][ col ] = player ;
// ✅ Correct - creates new board
const newBoard = [
[ ... state . board [ 0 ]],
[ ... state . board [ 1 ]],
[ ... state . board [ 2 ]],
];
newBoard [ row ][ col ] = player ;
4. Grid Rendering
Use nested maps to render 2D arrays:
renderBoard ( state ) {
return h ( "table" , {}, [
h ( "tbody" , {},
state . board . map (( row , i ) =>
h ( "tr" , {},
row . map (( cell , j ) =>
h ( "td" , {}, [ this . renderCell ( cell , i , j )])
)
)
)
)
]);
}
Game Flow
Initial State
Game starts with empty 3x3 board, player X goes first
Player Move
Player clicks a cell, triggering mark() method
State Update
Reducer validates move, updates board, checks for winner
Re-render
Component re-renders with new state
Game End
If winner or draw detected, board is frozen
Features Demonstrated
Reducer pattern for complex state transitions
Immutable state updates
Derived state (winner, draw)
Win detection algorithms
Draw detection
Move validation
Player switching
Conditional rendering based on game state
Grid rendering with nested maps
Disabling board when game ends
Dynamic styling based on state
Separation of concerns (logic vs UI)
Reusable helper functions
Clear function naming
Running the Example
Clone the repository
git clone https://github.com/x0bd/glyphui.git
cd glyphui/examples/tictactoe
Open in browser
Open index.html in your browser:
Play the game
Click cells to make moves
Try to get three in a row
Test edge cases (invalid moves, draw)
Enhancements to Try
Reset Button Add a button to reset the game
Score Tracking Track wins for X and O
AI Opponent Implement computer player
Animations Add transition animations
Next Steps
Memory Game See another game implementation
State Management Learn advanced state patterns
Reducers Deep dive into reducer pattern
Events Master event handling