How do I create a simple Luffarschack (Five in a Row) game with plain HTML/CSS/JS?
I want to build a Luffarschack (Gomoku/Five in a Row) game for my website using plain HTML, CSS, and JavaScript. I need: - A clickable game board grid - Two-player turn-based gameplay (alternating X and O, or black and white stones) - Win detection (five in a row horizontally, vertically, or diagonally) - A simple, clean UI Please provide a complete, working implementation I can add to my website.
0
Luffarschack (Five in a Row) — Complete Implementation
Here's a clean, fully working single-file implementation:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Luffarschack — Five in a Row</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
h1 {
font-size: 2rem;
margin-bottom: 4px;
letter-spacing: 2px;
color: #e94560;
}
.subtitle {
font-size: 0.85rem;
color: #888;
margin-bottom: 18px;
}
.status-bar {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 14px;
font-size: 1.1rem;
min-height: 40px;
}
.status-bar .turn-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
background: #16213e;
padding: 6px 18px;
border-radius: 8px;
border: 2px solid #0f3460;
transition: border-color 0.3s;
}
.status-bar .turn-indicator.black-turn {
border-color: #e94560;
}
.status-bar .turn-indicator.white-turn {
border-color: #53d8fb;
}
.stone-preview {
width: 20px;
height: 20px;
border-radius: 50%;
display: inline-block;
}
.stone-preview.black { background: radial-gradient(circle at 35% 35%, #555, #111); }
.stone-preview.white { background: radial-gradient(circle at 35% 35%, #fff, #aaa); }
.winner-message {
font-size: 1.3rem;
font-weight: bold;
color: #e94560;
animation: pulse 0.8s ease-in-out infinite alternate;
}
@keyframes pulse {
from { opacity: 0.7; transform: scale(1); }
to { opacity: 1; transform: scale(1.05); }
}
.board-wrapper {
background: #16213e;
padding: 12px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.board {
display: grid;
gap: 0;
background: #c8a96e;
border: 3px solid #8b6914;
border-radius: 4px;
position: relative;
}
.cell {
width: 36px;
height: 36px;
border: 0.5px solid rgba(0,0,0,0.25);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
transition: background 0.15s;
}
.cell:hover {
background: rgba(255,255,255,0.15);
}
.cell.disabled {
cursor: default;
}
.cell.disabled:hover {
background: transparent;
}
.stone {
width: 28px;
height: 28px;
border-radius: 50%;
animation: placeStone 0.25s ease-out;
box-shadow: 1px 2px 4px rgba(0,0,0,0.5);
}
.stone.black {
background: radial-gradient(circle at 35% 35%, #555, #111);
}
.stone.white {
background: radial-gradient(circle at 35% 35%, #fff, #bbb);
box-shadow: 1px 2px 4px rgba(0,0,0,0.3);
}
.stone.winning {
animation: winGlow 0.6s ease-in-out infinite alternate;
}
.stone.black.winning {
box-shadow: 0 0 10px 3px #e94560;
}
.stone.white.winning {
box-shadow: 0 0 10px 3px #53d8fb;
}
@keyframes placeStone {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes winGlow {
from { transform: scale(1); }
to { transform: scale(1.1); }
}
.controls {
margin-top: 16px;
display: flex;
gap: 12px;
align-items: center;
}
button {
background: #e94560;
color: white;
border: none;
padding: 10px 28px;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
font-weight: 600;
}
button:hover { background: #c73652; transform: translateY(-1px); }
button:active { transform: translateY(0); }
.size-selector {
display: flex;
align-items: center;
gap: 8px;
color: #aaa;
font-size: 0.9rem;
}
.size-selector select {
background: #16213e;
color: #eee;
border: 1px solid #0f3460;
padding: 6px 10px;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
}
.scoreboard {
margin-top: 12px;
display: flex;
gap: 24px;
font-size: 0.95rem;
color: #aaa;
}
.scoreboard span { font-weight: bold; color: #eee; }
@media (max-width: 600px) {
.cell { width: 28px; height: 28px; }
.stone { width: 22px; height: 22px; }
h1 { font-size: 1.4rem; }
}
</style>
</head>
<body>
<h1>Luffarschack</h1>
<p class="subtitle">Gomoku — Five in a Row</p>
<div class="status-bar">
<div id="turnIndicator" class="turn-indicator black-turn">
<span id="stonePreview" class="stone-preview black"></span>
<span id="statusText">Black's turn</span>
</div>
</div>
<div class="board-wrapper">
<div id="board" class="board"></div>
</div>
<div class="controls">
<button id="resetBtn">New Game</button>
<div class="size-selector">
<label for="boardSize">Board:</label>
<select id="boardSize">
<option value="15" selected>15 × 15</option>
<option value="17">17 × 17</option>
<option value="19">19 × 19</option>
</select>
</div>
</div>
<div class="scoreboard">
⚫ Black: <span id="scoreBlack">0</span> |
⚪ White: <span id="scoreWhite">0</span> |
Draws: <span id="scoreDraws">0</span>
</div>
<script>
(() => {
// ── State ──────────────────────────────────────
let SIZE = 15;
const WIN_LENGTH = 5;
let grid = []; // 0 = empty, 1 = black, 2 = white
let currentPlayer = 1; // 1 = black, 2 = white
let gameOver = false;
let moveCount = 0;
let scores = { black: 0, white: 0, draws: 0 };
// ── DOM refs ───────────────────────────────────
const boardEl = document.getElementById('board');
const statusText = document.getElementById('statusText');
const stonePreview = document.getElementById('stonePreview');
const turnIndicator = document.getElementById('turnIndicator');
const resetBtn = document.getElementById('resetBtn');
const boardSizeSel = document.getElementById('boardSize');
const scoreBlackEl = document.getElementById('scoreBlack');
const scoreWhiteEl = document.getElementById('scoreWhite');
const scoreDrawsEl = document.getElementById('scoreDraws');
// ── Initialize / Reset ─────────────────────────
function initGame() {
SIZE = parseInt(boardSizeSel.value);
grid = Array.from({ length: SIZE }, () => Array(SIZE).fill(0));
currentPlayer = 1;
gameOver = false;
moveCount = 0;
renderBoard();
updateStatus();
}
function renderBoard() {
boardEl.innerHTML = '';
boardEl.style.gridTemplateColumns = `repeat(${SIZE}, 1fr)`;
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = r;
cell.dataset.col = c;
cell.addEventListener('click', onCellClick);
boardEl.appendChild(cell);
}
}
}
// ── Click handler ──────────────────────────────
function onCellClick(e) {
if (gameOver) return;
const r = parseInt(e.currentTarget.dataset.row);
const c = parseInt(e.currentTarget.dataset.col);
if (grid[r][c] !== 0) return;
// Place stone
grid[r][c] = currentPlayer;
moveCount++;
const color = currentPlayer === 1 ? 'black' : 'white';
const stone = document.createElement('div');
stone.className = `stone ${color}`;
e.currentTarget.appendChild(stone);
e.currentTarget.classList.add('disabled');
// Check win
const winCells = checkWin(r, c, currentPlayer);
if (winCells) {
gameOver = true;
highlightWin(winCells);
const name = currentPlayer === 1 ? 'Black' : 'White';
showWinner(`${name} wins! 🎉`);
if (currentPlayer === 1) scores.black++;
else scores.white++;
updateScoreboard();
disableAllCells();
return;
}
// Check draw
if (moveCount === SIZE * SIZE) {
gameOver = true;
scores.draws++;
updateScoreboard();
showWinner("It's a draw!");
return;
}
// Switch turn
currentPlayer = currentPlayer === 1 ? 2 : 1;
updateStatus();
}
// ── Win Detection ──────────────────────────────
function checkWin(row, col, player) {
const directions = [
[0, 1], // horizontal →
[1, 0], // vertical ↓
[1, 1], // diagonal ↘
[1, -1], // diagonal ↙
];
for (const [dr, dc] of directions) {
let cells = [[row, col]];
// Count forward
for (let i = 1; i < WIN_LENGTH; i++) {
const nr = row + dr * i;
const nc = col + dc * i;
if (nr < 0 || nr >= SIZE || nc < 0 || nc >= SIZE) break;
if (grid[nr][nc] !== player) break;
cells.push([nr, nc]);
}
// Count backward
for (let i = 1; i < WIN_LENGTH; i++) {
const nr = row - dr * i;
const nc = col - dc * i;
if (nr < 0 || nr >= SIZE || nc < 0 || nc >= SIZE) break;
if (grid[nr][nc] !== player) break;
cells.push([nr, nc]);
}
if (cells.length >= WIN_LENGTH) return cells;
}
return null;
}
// ── UI Helpers ─────────────────────────────────
function updateStatus() {
const name = currentPlayer === 1 ? 'Black' : 'White';
const color = currentPlayer === 1 ? 'black' : 'white';
statusText.textContent = `${name}'s turn`;
stonePreview.className = `stone-preview ${color}`;
turnIndicator.className = `turn-indicator ${color}-turn`;
}
function showWinner(msg) {
statusText.textContent = msg;
statusText.classList.add('winner-message');
// Remove after reset
}
function highlightWin(cells) {
for (const [r, c] of cells) {
const idx = r * SIZE + c;
const cellEl = boardEl.children[idx];
const stone = cellEl.querySelector('.stone');
if (stone) stone.classList.add('winning');
}
}
function disableAllCells() {
boardEl.querySelectorAll('.cell').forEach(c => c.classList.add('disabled'));
}
function updateScoreboard() {
scoreBlackEl.textContent = scores.black;
scoreWhiteEl.textContent = scores.white;
scoreDrawsEl.textContent = scores.draws;
}
// ── Events ─────────────────────────────────────
resetBtn.addEventListener('click', () => {
statusText.classList.remove('winner-message');
initGame();
});
boardSizeSel.addEventListener('change', () => {
statusText.classList.remove('winner-message');
initGame();
});
// ── Start ──────────────────────────────────────
initGame();
})();
</script>
</body>
</html>
What's Included
| Feature | Details |
|---|---|
| Board | Configurable 15×15, 17×17, or 19×19 grid styled like a Go/Gomoku board |
| Gameplay | Click to place alternating black/white stones |
| Win Detection | Checks all 4 directions (horizontal, vertical, both diagonals) from the last-placed stone |
| Win Highlight | Winning stones glow and pulse with animation |
| Draw Detection | Detects when the board is completely filled |
| Scoreboard | Tracks wins for each side and draws across games |
| Responsive | Smaller stones on mobile screens |
| Zero dependencies | Pure HTML + CSS + JS in a single file |
How to Use
- Copy the entire code into a
.htmlfile - Open it in any modern browser
- Click any intersection to place a stone — players alternate automatically
- First to get five in a row (any direction) wins
Key Design Decisions
- Win check is O(1) per move — only scans outward from the last-placed stone in 4 directions (max 8 cells checked per direction), not the entire board.
- IIFE wrapper keeps all variables out of global scope — safe to embed alongside other scripts.
- CSS
radial-gradientstones give a 3D appearance without any images.
Comments