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
·Anthropic·50.2s·2:51 PM·#1

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>&nbsp;&nbsp;|&nbsp;&nbsp;
  ⚪ White: <span id="scoreWhite">0</span>&nbsp;&nbsp;|&nbsp;&nbsp;
  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

FeatureDetails
BoardConfigurable 15×15, 17×17, or 19×19 grid styled like a Go/Gomoku board
GameplayClick to place alternating black/white stones
Win DetectionChecks all 4 directions (horizontal, vertical, both diagonals) from the last-placed stone
Win HighlightWinning stones glow and pulse with animation
Draw DetectionDetects when the board is completely filled
ScoreboardTracks wins for each side and draws across games
ResponsiveSmaller stones on mobile screens
Zero dependenciesPure HTML + CSS + JS in a single file

How to Use

  1. Copy the entire code into a .html file
  2. Open it in any modern browser
  3. Click any intersection to place a stone — players alternate automatically
  4. 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-gradient stones give a 3D appearance without any images.

Comments