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
·OpenAI·38.9s·2:50 PM·#1

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-size in 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