I want to create a browser-based Othello (Reversi) game for two human players using plain HTML, CSS, and JavaScript (no frameworks). The game should include: - An 8x8 game board with the standard starting position (2 white and 2 black pieces in the center) - Click-to-place mechanics for each player's turn - Valid move detection (pieces must outflank opponent pieces) - Automatic flipping of captured pieces - Turn switching between black and white players - Display of current player's turn - Game-over detection (when neither player can move) and score display Please provide complete, working code I can use directly in a single HTML file.
2-Player Othello Game
Here's a complete, working Othello game in a single HTML file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Othello (Reversi)</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #1a1a2e;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #eee;
}
h1 {
margin-bottom: 10px;
font-size: 2rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(135deg, #e2e2e2, #a0d468);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
#info-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 30px;
margin: 12px 0 16px;
font-size: 1.1rem;
}
.score-box {
display: flex;
align-items: center;
gap: 8px;
background: #16213e;
padding: 8px 18px;
border-radius: 10px;
border: 1px solid #0f3460;
}
.score-disc {
width: 22px;
height: 22px;
border-radius: 50%;
display: inline-block;
border: 2px solid #555;
}
.score-disc.black { background: radial-gradient(circle at 35% 35%, #555, #111); }
.score-disc.white { background: radial-gradient(circle at 35% 35%, #fff, #bbb); }
#turn-display {
padding: 8px 20px;
border-radius: 10px;
font-weight: bold;
font-size: 1.05rem;
transition: background 0.3s;
}
#turn-display.black-turn {
background: #222;
border: 2px solid #666;
}
#turn-display.white-turn {
background: #ddd;
color: #222;
border: 2px solid #aaa;
}
#board {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 2px;
background: #111;
padding: 2px;
border-radius: 8px;
box-shadow: 0 0 30px rgba(0, 100, 0, 0.3), 0 8px 32px rgba(0,0,0,0.5);
width: min(85vw, 520px);
height: min(85vw, 520px);
}
.cell {
background: #2d6a30;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
transition: background 0.15s;
}
.cell:hover {
background: #3a8a3e;
}
.cell.valid-move::after {
content: '';
width: 30%;
height: 30%;
background: rgba(255, 255, 255, 0.25);
border-radius: 50%;
position: absolute;
pointer-events: none;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.3); opacity: 1; }
}
.disc {
width: 80%;
height: 80%;
border-radius: 50%;
transition: transform 0.5s ease;
position: absolute;
}
.disc.black {
background: radial-gradient(circle at 35% 35%, #555, #111);
box-shadow: 0 2px 6px rgba(0,0,0,0.6);
}
.disc.white {
background: radial-gradient(circle at 35% 35%, #fff, #bbb);
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
.disc.flipping {
animation: flip 0.5s ease-in-out;
}
@keyframes flip {
0% { transform: rotateY(0deg) scaleX(1); }
50% { transform: rotateY(90deg) scaleX(0.1); }
100% { transform: rotateY(180deg) scaleX(1); }
}
.disc.placed {
animation: pop-in 0.3s ease-out;
}
@keyframes pop-in {
0% { transform: scale(0); opacity: 0; }
70% { transform: scale(1.15); }
100% { transform: scale(1); opacity: 1; }
}
#game-over-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
align-items: center;
justify-content: center;
z-index: 100;
}
#game-over-overlay.show {
display: flex;
}
#game-over-box {
background: #16213e;
padding: 40px 50px;
border-radius: 16px;
text-align: center;
box-shadow: 0 0 40px rgba(0,0,0,0.6);
border: 1px solid #0f3460;
}
#game-over-box h2 {
font-size: 1.8rem;
margin-bottom: 12px;
}
#game-over-box p {
font-size: 1.15rem;
margin-bottom: 20px;
color: #aaa;
}
#restart-btn, #restart-btn-top {
padding: 10px 30px;
font-size: 1rem;
border: none;
border-radius: 8px;
cursor: pointer;
background: linear-gradient(135deg, #0f3460, #533483);
color: white;
font-weight: bold;
transition: transform 0.15s, box-shadow 0.15s;
}
#restart-btn:hover, #restart-btn-top:hover {
transform: scale(1.05);
box-shadow: 0 4px 15px rgba(83, 52, 131, 0.5);
}
#restart-btn-top {
margin-top: 10px;
font-size: 0.9rem;
padding: 7px 20px;
}
#message {
height: 24px;
margin-top: 8px;
font-size: 0.95rem;
color: #f0c040;
text-align: center;
}
</style>
</head>
<body>
<h1>Othello</h1>
<div id="info-bar">
<div class="score-box">
<span class="score-disc black"></span>
<span>Black: <strong id="black-score">2</strong></span>
</div>
<div id="turn-display" class="black-turn">⬤ Black's Turn</div>
<div class="score-box">
<span class="score-disc white"></span>
<span>White: <strong id="white-score">2</strong></span>
</div>
</div>
<div id="board"></div>
<div id="message"></div>
<button id="restart-btn-top" onclick="initGame()">Restart Game</button>
<div id="game-over-overlay">
<div id="game-over-box">
<h2 id="winner-text">Game Over!</h2>
<p id="final-score"></p>
<button id="restart-btn" onclick="initGame()">Play Again</button>
</div>
</div>
<script>
// --- Constants ---
const SIZE = 8;
const EMPTY = 0;
const BLACK = 1;
const WHITE = 2;
const DIRECTIONS = [
[-1, -1], [-1, 0], [-1, 1],
[ 0, -1], [ 0, 1],
[ 1, -1], [ 1, 0], [ 1, 1]
];
// --- State ---
let board = [];
let currentPlayer = BLACK;
let gameActive = true;
let consecutivePasses = 0;
// --- DOM refs ---
const boardEl = document.getElementById('board');
const blackScoreEl = document.getElementById('black-score');
const whiteScoreEl = document.getElementById('white-score');
const turnDisplayEl = document.getElementById('turn-display');
const messageEl = document.getElementById('message');
const overlayEl = document.getElementById('game-over-overlay');
const winnerTextEl = document.getElementById('winner-text');
const finalScoreEl = document.getElementById('final-score');
// --- Initialize ---
function initGame() {
board = [];
for (let r = 0; r < SIZE; r++) {
board[r] = [];
for (let c = 0; c < SIZE; c++) {
board[r][c] = EMPTY;
}
}
// Standard starting position
board[3][3] = WHITE;
board[3][4] = BLACK;
board[4][3] = BLACK;
board[4][4] = WHITE;
currentPlayer = BLACK;
gameActive = true;
consecutivePasses = 0;
messageEl.textContent = '';
overlayEl.classList.remove('show');
renderBoard();
updateInfo();
}
// --- Render ---
function renderBoard() {
boardEl.innerHTML = '';
const validMoves = getValidMoves(currentPlayer);
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.dataset.row = r;
cell.dataset.col = c;
if (board[r][c] !== EMPTY) {
const disc = document.createElement('div');
disc.classList.add('disc', board[r][c] === BLACK ? 'black' : 'white');
cell.appendChild(disc);
}
if (gameActive && validMoves.some(m => m[0] === r && m[1] === c)) {
cell.classList.add('valid-move');
}
cell.addEventListener('click', () => handleCellClick(r, c));
boardEl.appendChild(cell);
}
}
}
function updateInfo() {
const scores = getScores();
blackScoreEl.textContent = scores.black;
whiteScoreEl.textContent = scores.white;
if (gameActive) {
if (currentPlayer === BLACK) {
turnDisplayEl.textContent = '⬤ Black\'s Turn';
turnDisplayEl.className = 'black-turn';
} else {
turnDisplayEl.textContent = '◯ White\'s Turn';
turnDisplayEl.className = 'white-turn';
}
}
}
function getScores() {
let black = 0, white = 0;
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
if (board[r][c] === BLACK) black++;
else if (board[r][c] === WHITE) white++;
}
}
return { black, white };
}
// --- Game Logic ---
function opponent(player) {
return player === BLACK ? WHITE : BLACK;
}
function inBounds(r, c) {
return r >= 0 && r < SIZE && c >= 0 && c < SIZE;
}
// Returns array of pieces that would be flipped in a given direction
function getFlipsInDirection(r, c, dr, dc, player) {
const opp = opponent(player);
const flips = [];
let cr = r + dr;
let cc = c + dc;
while (inBounds(cr, cc) && board[cr][cc] === opp) {
flips.push([cr, cc]);
cr += dr;
cc += dc;
}
// Must end with the current player's piece to be valid
if (flips.length > 0 && inBounds(cr, cc) && board[cr][cc] === player) {
return flips;
}
return [];
}
// All flips for placing a piece at (r, c)
function getAllFlips(r, c, player) {
if (board[r][c] !== EMPTY) return [];
let allFlips = [];
for (const [dr, dc] of DIRECTIONS) {
allFlips = allFlips.concat(getFlipsInDirection(r, c, dr, dc, player));
}
return allFlips;
}
function isValidMove(r, c, player) {
return board[r][c] === EMPTY && getAllFlips(r, c, player).length > 0;
}
function getValidMoves(player) {
const moves = [];
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
if (isValidMove(r, c, player)) {
moves.push([r, c]);
}
}
}
return moves;
}
function makeMove(r, c, player) {
const flips = getAllFlips(r, c, player);
if (flips.length === 0) return false;
board[r][c] = player;
for (const [fr, fc] of flips) {
board[fr][fc] = player;
}
return flips;
}
// --- Click Handler ---
function handleCellClick(r, c) {
if (!gameActive) return;
if (!isValidMove(r, c, currentPlayer)) return;
const flips = makeMove(r, c, currentPlayer);
if (!flips) return;
consecutivePasses = 0;
messageEl.textContent = '';
// Render and animate
renderBoard();
animatePlacement(r, c);
animateFlips(flips);
// Switch turns
currentPlayer = opponent(currentPlayer);
// Check if next player has valid moves
if (getValidMoves(currentPlayer).length === 0) {
// Pass turn
consecutivePasses++;
if (consecutivePasses >= 2 || isBoardFull()) {
endGame();
return;
}
const skippedColor = currentPlayer === BLACK ? 'Black' : 'White';
messageEl.textContent = `${skippedColor} has no valid moves — turn skipped!`;
currentPlayer = opponent(currentPlayer);
// Check again — both stuck means game over
if (getValidMoves(currentPlayer).length === 0) {
endGame();
return;
}
} else {
consecutivePasses = 0;
}
renderBoard();
updateInfo();
}
function animatePlacement(r, c) {
const index = r * SIZE + c;
const cell = boardEl.children[index];
const disc = cell.querySelector('.disc');
if (disc) disc.classList.add('placed');
}
function animateFlips(flips) {
flips.forEach(([fr, fc]) => {
const index = fr * SIZE + fc;
const cell = boardEl.children[index];
const disc = cell.querySelector('.disc');
if (disc) disc.classList.add('flipping');
});
}
function isBoardFull() {
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
if (board[r][c] === EMPTY) return false;
}
}
return true;
}
function endGame() {
gameActive = false;
const scores = getScores();
let winner;
if (scores.black > scores.white) {
winner = '⬤ Black Wins!';
} else if (scores.white > scores.black) {
winner = '◯ White Wins!';
} else {
winner = "It's a Tie!";
}
turnDisplayEl.textContent = 'Game Over';
turnDisplayEl.className = '';
winnerTextEl.textContent = winner;
finalScoreEl.textContent = `Black ${scores.black} — ${scores.white} White`;
// Small delay so the last move animates before overlay
setTimeout(() => {
overlayEl.classList.add('show');
}, 600);
}
// --- Start ---
initGame();
</script>
</body>
</html>
How It Works
Board Representation
- An 8×8 2D array where
0 = empty,1 = black,2 = white - Standard opening: two black and two white discs placed diagonally in the center
Valid Move Detection
For each empty cell, the algorithm checks all 8 directions (horizontal, vertical, diagonal). A move is valid if placing a disc there outflanks at least one line of opponent discs — meaning there's a contiguous line of opponent pieces bookended by your own piece.
Core Game Loop
- Compute valid moves for the current player and highlight them with pulsing dots
- On click, place the disc and flip all outflanked opponent pieces
- Switch turns — if the next player has no valid moves, their turn is automatically skipped with a message
- Game ends when neither player can move (or the board is full), and the overlay shows the winner
Key Features
| Feature | Implementation |
|---|---|
| Valid move hints | Pulsing translucent dots via CSS animation |
| Disc placement | pop-in scale animation |
| Piece flipping | rotateY CSS flip animation |
| Turn indicator | Dynamic styling changes between black/white themes |
| Score tracking | Real-time count of all discs on the board |
| Game over | Modal overlay with final score and replay button |
Simply save the code as an .html file and open it in any modern browser to play!
Comments