- A win-checking system (
calculateWinner
) and draw detection. - A way to visually highlight winning cells.
- UI feedback: who's the winner or if it's a draw.
- Smarter board updates - because once someone wins, that board should lock down!
So, you've built the core of your Tic-Tac-Toe game using LWC. Players can click on cells, Xs and Os are rendered, and it's starting to feel like an actual game. But here comes the twist - without proper game rules and victory detection, all you've got is a fancy digital Etch A Sketch β
In this chapter, we're dialing things up. We're adding real game logic - where the app knows when someone wins, highlights the winning line, and even declares a draw when the board fills up. That means new logic, a few smart tweaks to our components, and of course, a way to show players that something awesome just happened. Let's break it all down.
What Changed from the Previous Version?
If you've been following along (and I hope you have!), you'll recognize the main components: gameContainer
, boardComponent
, cellComponent
, and our trusty logic in gameLogic.js
. In this step, we aren't reinventing the wheel - we're just adding that sweet, sweet game intelligence.
Specifically, we're introducing:
Let's look at what's new or updated.
β The Game Logic: gameLogic.js
Here's where the magic starts. We've added two key functions:
// Check for a winner among all possible lines
export function calculateWinner(cells) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (let [a, b, c] of lines) {
if (cells[a].value && cells[a].value === cells[b].value && cells[a].value === cells[c].value) {
return {
winner: cells[a].value,
winningLine: [a, b, c]
};
}
}
return null;
}
// If the board is full and no one has won, it's a draw
export function isDraw(cells) {
return cells.every(cell => cell.value) && !calculateWinner(cells);
}
export function generateInitialBoard() {
return Array.from({ length: 9 }, (_, index) => ({
id: index,
value: ''
}));
}
What's happening here?
We're checking all possible winning combinations - horizontal, vertical, diagonal. If three of the same values (X or O) line up, we return the winner and the indices of the winning cells.
The isDraw
function is a backup plan. If the board is full and there's no winner, it's a draw. Plain and simple.
β Smarter gameContainer
This component is now the brains of the operation. We added state tracking for:
- Whether the game is over
- The current message (who's up next, who won, etc.)
- The winning cells (for highlighting)
import {LightningElement, track} from 'lwc';
import {calculateWinner, generateInitialBoard, isDraw} from 'c/gameLogic';
export default class GameContainer extends LightningElement {
@track cells = generateInitialBoard(); // Create fresh board
@track gameOver = false; // Prevents moves after game ends
@track message = 'Current Player: X'; // Dynamic status message
@track winningLine = []; // Highlighted line when a player wins
currentPlayer = 'X'; // Track whose turn it is
handleCellClick(event) {
if (this.gameOver) return; // Don't allow clicks if game is over
const {cellId} = event.detail;
const targetCell = this.cells[cellId];
if (targetCell.value) return; // Ignore already filled cells
// Update board immutably
this.cells = this.cells.map(cell =>
cell.id === cellId ? {...cell, value: this.currentPlayer} : cell
);
// Check if this move wins the game
const result = calculateWinner(this.cells);
if (result) {
this.winningLine = result.winningLine;
this.gameOver = true;
this.message = `Player ${result.winner} wins!`;
return;
}
// Check for draw
if (isDraw(this.cells)) {
this.gameOver = true;
this.message = 'It\'s a draw!';
return;
}
// No winner or draw? Switch turn
this.currentPlayer = this.currentPlayer === 'X' ? 'O' : 'X';
this.message = `Current Player: ${this.currentPlayer}`;
}
}
Let's pause for a sec.
Notice how we now use @track
for multiple state values? We need LWC to rerender when these things change - especially the board and message.
Also, calculateWinner
and isDraw
control the flow. Once a win or draw is detected, gameOver
flips, and we stop accepting new moves. Boom. Solid.
β Highlighting the Winning Line β boardComponent
Okay, it's one thing to say βPlayer X wins.β But what if we could show it? Now the boardComponent
maps through the cells and tags the ones that are part of a winning combination:
export default class BoardComponent extends LightningElement {
@api cells = [];
@api winningLine = []; // IDs of winning cells to highlight
get rows() {
const rows = [];
for (let i = 0; i < this.cells.length; i += 3) {
const rowCells = this.cells.slice(i, i + 3).map(cell => ({
...cell,
isWinning: this.winningLine.includes(cell.id) // Flag winning cells
}));
rows.push({
id: `row-${i / 3}`,
cells: rowCells
});
}
return rows;
}
handleCellClick(event) {
const cellId = event.target.cellId;
this.dispatchEvent(new CustomEvent('cellclick', {detail: {cellId}}));
}
}
The real trick here is isWinning
, which gets passed down to each cellComponent
. It's a boolean that's true only if the cell is in the winning line.
β Visual Feedback β cellComponent
Our cells now shine-literally. Check this out:
export default class CellComponent extends LightningElement {
@api cellId;
@api value;
@api isWinning; // Received from boardComponent
handleClick() {
if (!this.value) {
this.dispatchEvent(
new CustomEvent('click', {
bubbles: true,
composed: true
})
);
}
}
get cellClass() {
// Add 'winning' class for victory highlight
return `slds-m-around_xxx-small cell ${this.isWinning ? 'winning' : ''}`;
}
}
.cell {
height: 60px;
border: 1px solid #ccc;
text-align: center;
font-size: 24px;
line-height: 60px;
cursor: pointer;
user-select: none;
}
.cell.winning {
background-color: #d4f5d4;
font-weight: bold;
border: 2px solid green;
}
Result? When you win, your three-in-a-row lights up in green. It's instant feedback and gives that satisfying yes! A moment players crave.
No Changes? No Mention.
Just to clarify - we didn't touch ticTacToe
or the base layout. Those are still doing their job just fine. The focus here is on logic, state, and feedback. All killer, no filler.
Wrapping It Up
With these changes, your Tic-Tac-Toe app isn't just playable - it's smart. It knows when someone wins, it knows when it's a draw, and it knows when to stop taking input. Most importantly, it feels complete. A game without rules is just chaos. Now? You've brought order.
Your users will love the immediate feedback, the green glow of victory, and the clear game messages. And from a dev's point of view, your logic is clean, modular, and easy to build upon.
What's next? Well, how about letting users rewind and replay moves? Stay tuned - we're diving into Move History in the next article.