I’m building a Tic-Tac-Toe game as part of The Odin Project JavaScript curriculum. My implementation works fine in the console version, but I’m facing an issue in the browser version.
function GameBoard() {
let rows = 3;
let columns = 3;
const board = [];
for(let i = 0; i < rows; i++) {
board[i] = [];
for(let j = 0; j < columns; j++) {
board[i].push(Cell());
}
}
const getBoard = () => board;
const markCell = (row, col, token) => {
if(board[row][col].getValue() != 0) return -1;
board[row][col].setValue(token);
return 0;
};
const printBoard = () => {
const boardWithCellValues = board.map((row) => row.map((cell) => cell.getValue()));
console.log(boardWithCellValues);
};
return {
getBoard,
markCell,
printBoard,
};
}
function Cell() {
let value = '';
const setValue = (token) => {
value = token;
};
const getValue = () => value;
return {
setValue,
getValue,
};
}
function GameController(
playerOne="Player 1",
playerTwo="Player 2"
) {
let turnsPlayed = 0;
const players = [
{
name: playerOne,
token: 'X'
},
{
name: playerTwo,
token: 'O'
}
];
const board = GameBoard();
let activePlayer = players[0];
const switchPlayerTurn = () => {
activePlayer = activePlayer === players[0] ? players[1] : players[0];
};
const getActivePlayer = () => activePlayer;
const printNewRound = () => {
board.printBoard();
console.log(`${getActivePlayer().name}'s turn ...`);
}
const checkWin = () => {
// logic for tic tac toe game
let hasWon = false;
let playerToken = getActivePlayer().token;
const boardArray = board.getBoard();
let a = boardArray[0][0].getValue();
let b = boardArray[1][1].getValue();
let c = boardArray[2][2].getValue();
if(a === b && b === c && a === playerToken) {
hasWon = true;
} else {
a = boardArray[0][2].getValue();
b = boardArray[1][1].getValue();
c = boardArray[2][0].getValue();
if(a === b && b === c && a === playerToken) {
hasWon = true;
} else {
for(let i = 0; i < boardArray.length; i++) {
a = boardArray[i][0].getValue();
b = boardArray[i][1].getValue();
c = boardArray[i][2].getValue();
if(a === b && b === c && a === playerToken) {
hasWon = true;
break;
}
a = boardArray[0][i].getValue();
b = boardArray[1][i].getValue();
c = boardArray[2][i].getValue();
if(a === b && b === c && a === playerToken) {
hasWon = true;
break;
}
}
}
}
return hasWon;
}
const checkDraw = () => {
return turnsPlayed == 9;
}
const playRound = (r, c) => {
console.log(
`Marking ${getActivePlayer().name}'s token in Cell (${r}, ${c}).`
);
const exit_status = board.markCell(r,c, getActivePlayer().token);
// Do nothing if an occupied cell is marked by player
if(exit_status == -1) return;
turnsPlayed++;
if(checkWin()) {
board.printBoard();
console.log(`${getActivePlayer().name} has won the game.`);
} else if (checkDraw()) {
board.printBoard();
console.log(`It is a Draw. Play again ...`)
} else {
switchPlayerTurn();
printNewRound();
}
}
// Intial Render
printNewRound();
return {
playRound,
getActivePlayer,
checkWin,
checkDraw,
getBoard: board.getBoard
};
}
function ScreenController() {
const game = GameController();
const playerTurnDiv = document.querySelector('.turn');
const boardDiv = document.querySelector('.board');
const updateScreen = () => {
boardDiv.textContent = '';
const board = game.getBoard();
const activePlayer = game.getActivePlayer();
playerTurnDiv.textContent = `${activePlayer.name}'s Turn`;
board.forEach((row, row_idx) => {
row.forEach((cell, col_idx) => {
const cellButton = document.createElement('button');
cellButton.classList.add('cell');
cellButton.textContent = cell.getValue();
cellButton.dataset.row_idx = row_idx;
cellButton.dataset.col_idx = col_idx;
boardDiv.appendChild(cellButton);
})
})
}
const displayGameOver = () => {
const msg = document.querySelector('.game-over-message');
if(game.checkWin()) {
msg.textContent = `${game.getActivePlayer().name} has won the game.`
} else {
msg.textContent = "It is a tie."
}
gameOverDialog.showModal();
}
function clickHandlerBoard(e) {
const row = e.target.dataset.row_idx;
const col = e.target.dataset.col_idx;
if(!row) return;
game.playRound(row, col);
updateScreen();
if(game.checkWin() || game.checkDraw())
displayGameOver();
}
boardDiv.addEventListener("click", clickHandlerBoard);
// Initial Render
updateScreen();
}
ScreenController();
const gameOverDialog = document.querySelector("#game-over-screen");
const restartBtn = document.querySelector('.restartBtn');
restartBtn.addEventListener('click', () => {
gameOverDialog.close();
ScreenController();
});
/*
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
@media (prefers-reduced-motion: no-preference) {
html {
interpolate-size: allow-keywords;
}
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
p {
text-wrap: pretty;
}
h1, h2, h3, h4, h5, h6 {
text-wrap: balance;
}
#root, #__next {
isolation: isolate;
}
/* CSS Starts Here */
.main-container {
height: 100vh;
display: flex;
flex-direction: column;
font-family: monospace, sans-serif;
}
.header {
padding: 32px;
text-align: center;
}
.header h1 {
font-size: 48px;
}
.footer {
text-align: center;
padding: 16px;
}
.container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 36px 0;
gap: 1.5em;
}
.container .turn {
font-size: 24px;
}
.board {
display: grid;
height: 500px;
width: 500px;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
border: 1px solid black;
gap: 2px;
padding: 1px;
background-color: rgb(255, 17, 0);
}
.cell {
display: flex;
justify-content: center;
align-items: center;
font-size: 5rem;
border: 1px solid grey;
background: lightyellow;
cursor: pointer;
}
dialog {
font-size: 2em;
position: fixed;
margin: auto;
width: 45ch;
/* padding: 1em; */
border: 3px solid greenyellow;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
.game-over-message {
margin-bottom: 1.5em;
text-align: center;
}
.game-over-toolbar {
display: flex;
justify-content: center;
}
.restartBtn {
padding: 0.5em;
border-radius: 32px;
border: none;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
background-color: rgb(173, 255, 47);
display: flex;
align-items: center;
justify-content: center;
}
.restartBtn:hover {
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
cursor: pointer;
}
<div class="main-container">
<div class="header">
<h1>Tic Tac Toe</h1>
</div>
<div class="container">
<div class="turn"></div>
<div class="board"></div>
</div>
<div class="footer">© GOTW 2025</div>
</div>
<dialog id="game-start-screen">
<label for="playerOneName">Enter Player 1's name:</label>
<input type="text" id="playerOneName" autofocus>
</dialog>
<dialog id="game-over-screen">
<div class="game-over-message"></div>
<div class="game-over-toolbar">
<button class="restartBtn">↻ Restart</button>
</div>
</dialog>
</body>
Problem:
After a game is won, I display a “Game Over” dialog. I’ve added a Restart button inside this dialog, which is meant to reset the game state and allow a fresh game. However, after clicking restart, although the dialog closes, the game remains stuck — it behaves as if the previous state is still active, and clicking on a cell immediately brings back the “Game Over” dialog.
Here’s the relevant part of the code:
restartBtn.addEventListener('click', () => {
gameOverDialog.close();
ScreenController(); });
Question:
How can I properly reset both the UI and internal game state when restarting the game via the Restart button? Is there a better way to reinitialize the game components?
Any suggestions or patterns for managing game state reset cleanly would be appreciated!
What I expected:
- The board should be cleared.
- All game state (turns, board data, game over flag) should reset.
- The user should be able to play a new game from scratch.
What I’ve tried:
- Calling ScreenController() again in the restart handler.
- Confirmed that the console version resets just fine.



