______ ______ __ __ ______ __ __ ______ /\ __ \ /\__ _\ /\ \_\ \ /\ ___\ /\ \ /\ \ /\ __ \ \ \ \/\ \ \/_/\ \/ \ \ __ \ \ \ __\ \ \ \____ \ \ \____ \ \ \/\ \ \ \_____\ \ \_\ \ \_\ \_\ \ \_____\ \ \_____\ \ \_____\ \ \_____\ \/_____/ \/_/ \/_/\/_/ \/_____/ \/_____/ \/_____/ \/_____/ -------------------------------------------------------------------------- A browser-based recreation of the popular board game Othello.
TRY IT OUT HERE!
Featuring a colorful yet simplistic UI created utilizing minimal HTML and CSS, this version offers flexible support for three different gamemodes to choose from:
- Player v. Player: Play on a single computer with a friend, simply passing control of the mouse to the other person.
- Player v. AI: Play against an AI which utilizes a minimax algorithm and evaluates game states with a simple heuristic.
- Player v. Player (Online): Hit create a lobby to generate a code to share with a friend, who can then join with the code to commence online play. Feature created with the WebSocket protocol, through the Socket.io JS package.
The original version of this project I had created, written in Python, can be found here.
function humanTurn(currentPlayer, pos) {
if (!currentPlayer.moves.includes(pos)) {
shake(document.getElementById("moveStatus"), "INVALID MOVE!", "CLICK A SQUARE!");
return false;
} else {
let brackets = playMove(currentPlayer, pos, BOARD);
alterScore(currentPlayer.color, brackets);
alterStyles(currentPlayer.color, brackets, Player1.score, Player2.score);
currentPlayer.opponent.moves = getAllLegalMoves(currentPlayer.opponent, BOARD);
if (currentPlayer.opponent.moves.length > 0) {
turn = !turn;
} else {
currentPlayer.moves = getAllLegalMoves(currentPlayer, BOARD);
if (currentPlayer.moves.length == 0) {
endGame();
}
}
return true;
}
}
A function which takes in the currentPlayer
object and the pos
of the cell the player clicked.
- If
pos
is not in the list of legal moves, a visual effect is triggered. Otherwise, the scores and board are updated, and thecurrentPlayer.opponent
list of legal moves are updated. - If the opponent has no moves, the
currentPlayer
can pick a cell again; however, if they have no moves either,endGame()
is called.
function h(board) {
let whiteCount = 0;
let blackCount = 0;
for (let i = 0; i < 64; i++) {
if (board[i] == 'B') {
blackCount++;
} else if (board[i] == 'W') {
whiteCount++;
}
}
return whiteCount - blackCount;
}
The AI follows the rule of maximizing the amount of game pieces of its own color; thus, if its game pieces are black, whiteCount - blackCount
should be minimized; if its game pieces are white, whiteCount - blackCount
should be maximized.
function minimax(current, board, depth, maximizingPlayer, alpha, beta) {
if (depth == 0 || stopCondition(current, current.opponent) == true) {
return [h(board), null]
} else {
const potentialMoves = current.moves.slice();
if (maximizingPlayer) {
let bestVal = [-Infinity, null];
for (let i = 0; i < potentialMoves.length; i++) {
const tempBoard = board.slice();
playMove(current, potentialMoves[i], tempBoard);
current.opponent.moves = getAllLegalMoves(current.opponent, tempBoard);
let v = [minimax(current.opponent, tempBoard, depth-1, false, alpha, beta)[0], potentialMoves[i]];
bestVal = myMax(v, bestVal);
alpha = Math.max(alpha, bestVal[0]);
if (beta <= alpha) {
break;
}
}
return bestVal;
} else {
let bestVal = [Infinity, null];
for (let i = 0; i < potentialMoves.length; i++) {
const tempBoard = board.slice();
playMove(current, potentialMoves[i], tempBoard);
current.opponent.moves = getAllLegalMoves(current.opponent, tempBoard);
let v = [minimax(current.opponent, tempBoard, depth-1, true, alpha, beta)[0], potentialMoves[i]];
bestVal = myMin(v, bestVal);
beta = Math.min(beta, bestVal[0]);
if (beta <= alpha) {
break;
}
}
return bestVal;
}
}
}
export function botPlayMove(player, board) {
let optimalPos = minimax(player, board, 3, player.strategy, -Infinity, Infinity)[1];
return playMove(player, optimalPos, board);
}
This recursive minimax()
function returns a tuple as opposed to a singular value.
- This tuple is comprised of the value of the heuristic evaluated on a board on which the "optimal" position was played, and the actual optimal position itself.
- Then,
botPlayMove()
calls upon theminimax
function to select anoptimalPos
, and then callsplayMove()
with thatoptimalPos
. - In the actual gameplay loop, similar logic is followed as described in the Player v. Player code above; the key difference is that
botPlayMove()
is called following legal play by the (human) player.
async function gameLoop(roomID, gameSocket) {
const sockets = await io.in(roomID).fetchSockets();
const players = [...sockets];
gameSocket.on('playerClickCell', (pos) => {
gameSocket.emit('tryPos', pos);
});
gameSocket.on('passTurn', (pos) => {
gameSocket.emit('awaitTurn');
turn = !turn;
let data = {
pos: pos,
turn: turn,
flag: true
}
gameSocket.to(roomID).emit('yourTurn', data)
})
gameSocket.on('alterScoreFlag', (data) => {
io.in(roomID).emit('alterScore', data);
});
gameSocket.on('alterStylesFlag', (data) => {
io.in(roomID).emit('alterStyles', data);
})
gameSocket.on('endGameFlag', () => {
io.in(roomID).emit('endGame');
})
}
Above are all the server-side event listeners and emitters that allow for bidirectional communication with clients in the game room.
- On
'playerClickCell'
, the server tells the client to play that position, and if the move is legal, the client emits 'passTurn', 'alterScoreFlag', and 'alterStylesFlag'. - On
'alterScoreFlag'
and'alterStylesFlag'
, the server emits'alterScore'
and'alterStyles'
to all clients in the room, updating the physical scoreboard and board, alongside the javascript variables associated with each, on their end. - On
'passTurn'
, the server tells the client (who just played a position) to'awaitTurn'
, disabling their ability to click the board. The server then tells the other client it is'yourTurn'
, re-enabling their ability to click the board. - On
'endGameFlag'
, the server emits'endGame'
to all clients in the room, upon which they call theendGame()
function.
- Client: HTML, CSS, JavaScript
- Server: Node.js, Express, Socket.io
- Set up a CI/CD Pipeline for my own sanity
- Fix bugginess with the
endGame()
function not calling when it needs to - Indicator to alert the user if their opponent has left the game
- Add a selector for the difficulty of the AI, implemented through the usage of different heuristic functions
- Ensure the ability to play over long distances without weird behavior