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
Below is a complete, drop-in plain HTML/CSS/JS implementation of Luffarschack (Gomoku / Five in a Row) with:
- Clickable grid board
- Two-player alternating turns (Black/White)
- Win detection for 5 in a row (horizontal/vertical/diagonals)
- Simple UI + restart button
- Highlights the winning 5 stones
Copy this into a single .html file and open it, or paste into your site.
Complete working implementation (single file)
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Luffarschack (Gomoku) – Five in a Row</title>
<style>
:root {
--board-bg: #d9a85b;
--grid-line: rgba(0,0,0,.25);
--cell-size: 34px; /* change for bigger/smaller board */
--stone-pad: 6px;
}
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
margin: 0;
background: #0f1115;
color: #e9eef6;
display: grid;
place-items: center;
min-height: 100vh;
padding: 24px;
}
.app {
width: min(92vw, 720px);
display: grid;
gap: 12px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.status {
font-weight: 600;
letter-spacing: .2px;
}
button {
appearance: none;
border: 1px solid rgba(255,255,255,.18);
background: rgba(255,255,255,.06);
color: #e9eef6;
padding: 10px 12px;
border-radius: 10px;
cursor: pointer;
}
button:hover { background: rgba(255,255,255,.10); }
button:active { transform: translateY(1px); }
.boardWrap {
background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.12);
border-radius: 14px;
padding: 14px;
overflow: auto;
}
/* Board grid */
.board {
--n: 15; /* default size; JS will set this */
display: grid;
grid-template-columns: repeat(var(--n), var(--cell-size));
grid-template-rows: repeat(var(--n), var(--cell-size));
background: var(--board-bg);
border-radius: 10px;
position: relative;
box-shadow: 0 10px 30px rgba(0,0,0,.35);
}
.cell {
width: var(--cell-size);
height: var(--cell-size);
border-right: 1px solid var(--grid-line);
border-bottom: 1px solid var(--grid-line);
display: grid;
place-items: center;
cursor: pointer;
user-select: none;
position: relative;
}
/* remove last row/col borders for neatness */
.cell[data-last-col="1"] { border-right: none; }
.cell[data-last-row="1"] { border-bottom: none; }
.cell:focus-visible {
outline: 3px solid rgba(120,170,255,.8);
outline-offset: -3px;
}
.stone {
width: calc(var(--cell-size) - var(--stone-pad) * 2);
height: calc(var(--cell-size) - var(--stone-pad) * 2);
border-radius: 999px;
box-shadow: inset 0 2px 6px rgba(255,255,255,.18),
inset 0 -4px 10px rgba(0,0,0,.35),
0 2px 6px rgba(0,0,0,.35);
}
.black { background: radial-gradient(circle at 30% 30%, #555, #0b0b0b 70%); }
.white { background: radial-gradient(circle at 30% 30%, #fff, #cfcfcf 70%); }
.win {
outline: 3px solid rgba(0, 255, 170, .85);
outline-offset: 2px;
border-radius: 999px;
}
.hint {
font-size: 13px;
opacity: .85;
line-height: 1.35;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
select {
background: rgba(255,255,255,.06);
color: #e9eef6;
border: 1px solid rgba(255,255,255,.18);
padding: 10px 12px;
border-radius: 10px;
}
</style>
</head>
<body>
<div class="app">
<div class="topbar">
<div class="status" id="status">Loading…</div>
<div class="controls">
<label>
<span class="hint">Board</span><br />
<select id="sizeSelect" aria-label="Board size">
<option value="10">10×10</option>
<option value="13">13×13</option>
<option value="15" selected>15×15</option>
<option value="19">19×19</option>
</select>
</label>
<button id="restartBtn" type="button">Restart</button>
</div>
</div>
<div class="boardWrap">
<div id="board" class="board" role="grid" aria-label="Gomoku board"></div>
</div>
<div class="hint">
Click a cell to place a stone. First to get <b>five in a row</b> wins (horizontal, vertical, or diagonal).
</div>
</div>
<script>
const boardEl = document.getElementById('board');
const statusEl = document.getElementById('status');
const restartBtn = document.getElementById('restartBtn');
const sizeSelect = document.getElementById('sizeSelect');
const EMPTY = 0, BLACK = 1, WHITE = 2;
let N = parseInt(sizeSelect.value, 10);
let grid = [];
let current = BLACK;
let gameOver = false;
function initGame() {
N = parseInt(sizeSelect.value, 10);
grid = Array.from({ length: N }, () => Array(N).fill(EMPTY));
current = BLACK;
gameOver = false;
boardEl.style.setProperty('--n', N);
boardEl.innerHTML = '';
// Build cells
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.setAttribute('role', 'gridcell');
cell.tabIndex = 0;
cell.dataset.r = r;
cell.dataset.c = c;
if (c === N - 1) cell.dataset.lastCol = "1";
if (r === N - 1) cell.dataset.lastRow = "1";
cell.addEventListener('click', onCell);
cell.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onCell.call(cell);
}
});
boardEl.appendChild(cell);
}
}
updateStatus();
}
function updateStatus(extra = '') {
if (gameOver) return;
statusEl.textContent = `${current === BLACK ? 'Black' : 'White'} to move${extra}`;
}
function onCell() {
if (gameOver) return;
const r = parseInt(this.dataset.r, 10);
const c = parseInt(this.dataset.c, 10);
if (grid[r][c] !== EMPTY) return;
// Place stone
grid[r][c] = current;
renderStone(this, current);
// Check win
const winLine = checkWin(r, c, current);
if (winLine) {
gameOver = true;
statusEl.textContent = `${current === BLACK ? 'Black' : 'White'} wins!`;
highlightWin(winLine);
return;
}
// Check draw (optional)
if (isBoardFull()) {
gameOver = true;
statusEl.textContent = `Draw!`;
return;
}
// Next player
current = (current === BLACK) ? WHITE : BLACK;
updateStatus();
}
function renderStone(cellEl, player) {
const stone = document.createElement('div');
stone.className = `stone ${player === BLACK ? 'black' : 'white'}`;
cellEl.appendChild(stone);
}
function isBoardFull() {
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
if (grid[r][c] === EMPTY) return false;
}
}
return true;
}
// Returns an array of 5+ coordinates if win, else null
function checkWin(r, c, player) {
// Directions: (dr, dc)
const dirs = [
[0, 1], // horizontal
[1, 0], // vertical
[1, 1], // diag down-right
[1, -1], // diag down-left
];
for (const [dr, dc] of dirs) {
const line = collectLine(r, c, dr, dc, player);
if (line.length >= 5) return line; // highlight all in the line (>=5)
}
return null;
}
function collectLine(r, c, dr, dc, player) {
const coords = [[r, c]];
// forward
let rr = r + dr, cc = c + dc;
while (inBounds(rr, cc) && grid[rr][cc] === player) {
coords.push([rr, cc]);
rr += dr; cc += dc;
}
// backward
rr = r - dr; cc = c - dc;
while (inBounds(rr, cc) && grid[rr][cc] === player) {
coords.unshift([rr, cc]);
rr -= dr; cc -= dc;
}
return coords;
}
function inBounds(r, c) {
return r >= 0 && r < N && c >= 0 && c < N;
}
function highlightWin(coords) {
// Highlight only 5 if you prefer; here we highlight the whole connected line.
// To highlight exactly 5, slice around the placed stone.
for (const [r, c] of coords) {
const idx = r * N + c;
const cell = boardEl.children[idx];
const stone = cell.querySelector('.stone');
if (stone) stone.classList.add('win');
}
}
restartBtn.addEventListener('click', initGame);
sizeSelect.addEventListener('change', initGame);
initGame();
</script>
</body>
</html>
Easy tweaks
- Board size: change the dropdown options or default
15×15. - Cell size: adjust
--cell-sizein CSS. - Use X/O instead of stones: replace
renderStone()to insert text and style it.
If you want, I can also provide: single-player vs AI, “exactly five” win rules, move history/undo, or mobile-friendly drag-to-pan on large boards.
Comments