← back to Games Agentabrams

games/sudoku/index.html

800 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>NEON SUDOKU</title>
<style>
  :root{
    --bg:#0a0e14;
    --panel:#0f151f;
    --grid-line:#1c2a3a;
    --grid-line-strong:#2fd2ff;
    --given:#e6f4ff;
    --entry:#38f0c0;
    --note:#5f7a92;
    --conflict:#ff3b5c;
    --accent:#2fd2ff;
    --sel:#12303f;
    --sel-peer:#0e1f2b;
    --sameval:#123448;
  }
  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
  html,body{margin:0;padding:0;height:100%;}
  body{
    background:var(--bg);
    color:#cfe6f5;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:flex-start;
    min-height:100%;
    padding:12px 10px 24px;
    touch-action:manipulation;
    overflow-x:hidden;
  }
  h1{
    margin:4px 0 10px;
    font-size:clamp(18px,5vw,30px);
    font-weight:800;
    letter-spacing:.34em;
    text-transform:uppercase;
    color:#eaf7ff;
    text-shadow:0 0 8px rgba(47,210,255,.6),0 0 22px rgba(47,210,255,.25);
    text-align:center;
  }
  .hud{
    display:flex;
    flex-wrap:wrap;
    gap:8px 14px;
    align-items:center;
    justify-content:center;
    margin-bottom:12px;
    width:100%;
    max-width:560px;
  }
  .hud .item{
    display:flex;
    flex-direction:column;
    align-items:center;
    line-height:1.1;
  }
  .hud .label{
    font-size:10px;
    letter-spacing:.18em;
    text-transform:uppercase;
    color:#5f7a92;
  }
  .hud .val{
    font-size:16px;
    font-weight:700;
    font-variant-numeric:tabular-nums;
    color:var(--accent);
    text-shadow:0 0 8px rgba(47,210,255,.4);
  }
  .controls{
    display:flex;
    flex-wrap:wrap;
    gap:8px;
    justify-content:center;
    margin-bottom:14px;
    width:100%;
    max-width:560px;
  }
  select,button{
    font-family:inherit;
    background:var(--panel);
    color:#cfe6f5;
    border:1px solid var(--grid-line);
    border-radius:8px;
    padding:9px 12px;
    font-size:13px;
    letter-spacing:.05em;
    cursor:pointer;
    transition:border-color .15s,box-shadow .15s,background .15s;
  }
  select:focus,button:focus{outline:none;border-color:var(--accent);}
  button:hover,select:hover{border-color:var(--accent);}
  button:active{transform:translateY(1px);}
  button.primary{
    border-color:var(--accent);
    color:#eaf7ff;
    box-shadow:0 0 10px rgba(47,210,255,.25) inset;
  }
  button.toggled{
    background:#123448;
    border-color:var(--entry);
    color:var(--entry);
    box-shadow:0 0 10px rgba(56,240,192,.3) inset;
  }
  #boardWrap{
    width:min(94vw,520px);
    aspect-ratio:1/1;
    max-height:60vh;
    margin-bottom:16px;
  }
  #board{
    width:100%;
    height:100%;
    display:grid;
    grid-template-columns:repeat(9,1fr);
    grid-template-rows:repeat(9,1fr);
    background:var(--grid-line-strong);
    border:2px solid var(--grid-line-strong);
    border-radius:6px;
    gap:1px;
    box-shadow:0 0 22px rgba(47,210,255,.25);
    overflow:hidden;
  }
  .cell{
    position:relative;
    background:var(--panel);
    display:flex;
    align-items:center;
    justify-content:center;
    font-size:clamp(16px,5vw,28px);
    font-weight:600;
    color:var(--entry);
    cursor:pointer;
    user-select:none;
    font-variant-numeric:tabular-nums;
  }
  .cell.given{color:var(--given);font-weight:700;}
  /* thick 3x3 box borders using margins */
  .cell.br{margin-right:1px;box-shadow:2px 0 0 0 var(--grid-line-strong);}
  .cell.bb{margin-bottom:1px;box-shadow:0 2px 0 0 var(--grid-line-strong);}
  .cell.br.bb{box-shadow:2px 0 0 0 var(--grid-line-strong),0 2px 0 0 var(--grid-line-strong);}
  .cell.peer{background:var(--sel-peer);}
  .cell.sameval{background:var(--sameval);}
  .cell.selected{background:var(--sel);box-shadow:inset 0 0 0 2px var(--accent);}
  .cell.selected.br,.cell.selected.bb,.cell.selected.br.bb{box-shadow:inset 0 0 0 2px var(--accent);}
  .cell.conflict{color:var(--conflict);text-shadow:0 0 10px rgba(255,59,92,.7);}
  .cell.conflict.given{color:var(--conflict);}
  .notes{
    position:absolute;
    inset:2px;
    display:grid;
    grid-template-columns:repeat(3,1fr);
    grid-template-rows:repeat(3,1fr);
    pointer-events:none;
  }
  .notes span{
    display:flex;
    align-items:center;
    justify-content:center;
    font-size:clamp(7px,2vw,11px);
    font-weight:600;
    color:var(--note);
    line-height:1;
  }
  .pad{
    display:grid;
    grid-template-columns:repeat(5,1fr);
    gap:8px;
    width:min(94vw,520px);
    margin-bottom:10px;
  }
  .pad button{
    padding:14px 0;
    font-size:20px;
    font-weight:700;
    border-radius:10px;
  }
  .pad .num{color:#eaf7ff;}
  .pad .fn{font-size:12px;letter-spacing:.08em;}
  .msg{
    min-height:22px;
    font-size:14px;
    letter-spacing:.1em;
    text-transform:uppercase;
    color:var(--entry);
    text-shadow:0 0 10px rgba(56,240,192,.5);
    text-align:center;
    font-weight:700;
  }
  .best{font-size:11px;color:#5f7a92;letter-spacing:.12em;text-transform:uppercase;text-align:center;margin-top:2px;}
  @media (max-width:360px){
    h1{letter-spacing:.2em;}
    .pad button{padding:11px 0;font-size:17px;}
  }
</style>
</head>
<body>
  <h1>Neon Sudoku</h1>

  <div class="hud">
    <div class="item"><span class="label">Time</span><span class="val" id="timer">00:00</span></div>
    <div class="item"><span class="label">Difficulty</span><span class="val" id="diffLbl">Easy</span></div>
    <div class="item"><span class="label">Mode</span><span class="val" id="modeLbl">Fill</span></div>
  </div>

  <div class="controls">
    <select id="diffSel" aria-label="Difficulty">
      <option value="easy">Easy</option>
      <option value="medium">Medium</option>
      <option value="hard">Hard</option>
    </select>
    <button class="primary" id="newBtn">New Puzzle</button>
    <button id="notesBtn">Notes: Off</button>
  </div>

  <div id="boardWrap"><div id="board" aria-label="Sudoku board"></div></div>

  <div class="pad" id="pad"></div>

  <div class="msg" id="msg"></div>
  <div class="best" id="best"></div>

<script>
(function(){
  "use strict";

  var STORE_KEY = "abrams-sudoku";
  var SIZE = 9;

  // ---------- utilities ----------
  function shuffled(arr){
    var a = arr.slice();
    for (var i = a.length - 1; i > 0; i--){
      var j = Math.floor(Math.random() * (i + 1));
      var t = a[i]; a[i] = a[j]; a[j] = t;
    }
    return a;
  }

  // ---------- solution generator (bounded backtracking) ----------
  // Returns a full valid 9x9 grid (array of 81 ints 1-9), or null on failure.
  function generateSolution(){
    var grid = new Array(81).fill(0);
    var steps = { count: 0 };
    var MAX_STEPS = 600000; // hard cap so we can never spin forever

    function fits(idx, val){
      var r = (idx / 9) | 0, c = idx % 9;
      var br = r - r % 3, bc = c - c % 3;
      for (var i = 0; i < 9; i++){
        if (grid[r*9 + i] === val) return false;
        if (grid[i*9 + c] === val) return false;
      }
      for (var rr = 0; rr < 3; rr++){
        for (var cc = 0; cc < 3; cc++){
          if (grid[(br+rr)*9 + (bc+cc)] === val) return false;
        }
      }
      return true;
    }

    function solve(pos){
      if (steps.count++ > MAX_STEPS) return false;
      if (pos >= 81) return true;
      if (grid[pos] !== 0) return solve(pos + 1);
      var nums = shuffled([1,2,3,4,5,6,7,8,9]);
      for (var k = 0; k < 9; k++){
        var v = nums[k];
        if (fits(pos, v)){
          grid[pos] = v;
          if (solve(pos + 1)) return true;
          grid[pos] = 0;
        }
        if (steps.count > MAX_STEPS) return false;
      }
      return false;
    }

    if (solve(0)) return grid;
    return null;
  }

  // ---------- solver: count solutions up to a limit (for uniqueness) ----------
  function countSolutions(puzzle, limit){
    var grid = puzzle.slice();
    var steps = { count: 0 };
    var MAX_STEPS = 400000;
    var found = 0;

    function fits(idx, val){
      var r = (idx / 9) | 0, c = idx % 9;
      var br = r - r % 3, bc = c - c % 3;
      for (var i = 0; i < 9; i++){
        if (grid[r*9 + i] === val) return false;
        if (grid[i*9 + c] === val) return false;
      }
      for (var rr = 0; rr < 3; rr++){
        for (var cc = 0; cc < 3; cc++){
          if (grid[(br+rr)*9 + (bc+cc)] === val) return false;
        }
      }
      return true;
    }

    // find the empty cell with fewest candidates (MRV) to keep it fast/bounded
    function pickCell(){
      var best = -1, bestCount = 10;
      for (var idx = 0; idx < 81; idx++){
        if (grid[idx] !== 0) continue;
        var cnt = 0;
        for (var v = 1; v <= 9; v++){ if (fits(idx, v)) cnt++; }
        if (cnt < bestCount){ bestCount = cnt; best = idx; if (cnt <= 1) break; }
      }
      return best;
    }

    function solve(){
      if (steps.count++ > MAX_STEPS) return true; // bail -> treat as "give up", stops recursion
      var idx = pickCell();
      if (idx === -1){ found++; return found >= limit; }
      for (var v = 1; v <= 9; v++){
        if (fits(idx, v)){
          grid[idx] = v;
          if (solve()) { grid[idx] = 0; return true; }
          grid[idx] = 0;
        }
        if (steps.count > MAX_STEPS) return true;
      }
      return false;
    }

    solve();
    return found;
  }

  // ---------- build a puzzle at difficulty ----------
  var DIFF = {
    easy:   { clues: 40 },
    medium: { clues: 32 },
    hard:   { clues: 26 }
  };

  // Make puzzle: start from full solution, remove cells (symmetric) while
  // keeping a unique solution, bounded by attempts. Falls back gracefully.
  function makePuzzle(difficulty){
    var solution = null;
    // bounded retries to obtain a solution
    for (var g = 0; g < 8 && !solution; g++){
      solution = generateSolution();
    }
    if (!solution){
      // absolute fallback: a hard-coded valid solved grid
      solution = FALLBACK_SOLUTION.slice();
    }

    var targetClues = (DIFF[difficulty] || DIFF.easy).clues;
    var puzzle = solution.slice();

    var order = shuffled((function(){ var a=[]; for(var i=0;i<81;i++) a.push(i); return a; })());
    var removed = 0;
    var toRemove = 81 - targetClues;
    var attempts = 0;
    var MAX_ATTEMPTS = 200; // hard cap on removal attempts

    for (var oi = 0; oi < order.length && removed < toRemove && attempts < MAX_ATTEMPTS; oi++){
      var idx = order[oi];
      if (puzzle[idx] === 0) continue;
      attempts++;
      var saved = puzzle[idx];
      puzzle[idx] = 0;
      // keep unique: count solutions up to 2
      var cnt = countSolutions(puzzle, 2);
      if (cnt !== 1){
        puzzle[idx] = saved; // revert, not removable
      } else {
        removed++;
      }
    }

    return { puzzle: puzzle, solution: solution };
  }

  // A known-good completed grid used only if generation ever fails entirely.
  var FALLBACK_SOLUTION = [
    5,3,4, 6,7,8, 9,1,2,
    6,7,2, 1,9,5, 3,4,8,
    1,9,8, 3,4,2, 5,6,7,
    8,5,9, 7,6,1, 4,2,3,
    4,2,6, 8,5,3, 7,9,1,
    7,1,3, 9,2,4, 8,5,6,
    9,6,1, 5,3,7, 2,8,4,
    2,8,7, 4,1,9, 6,3,5,
    3,4,5, 2,8,6, 1,7,9
  ];

  // ---------- game state ----------
  var state = {
    puzzle: [],       // starting clues (0 = empty)
    solution: [],     // full solution
    values: [],       // current user values (includes givens)
    notes: [],        // array of Set-like objects per cell
    given: [],        // boolean per cell
    selected: -1,
    notesMode: false,
    difficulty: "easy",
    startTime: 0,
    elapsed: 0,
    running: false,
    solved: false
  };

  var timerHandle = null;

  // ---------- DOM refs ----------
  var boardEl = document.getElementById("board");
  var padEl = document.getElementById("pad");
  var timerEl = document.getElementById("timer");
  var diffLblEl = document.getElementById("diffLbl");
  var modeLblEl = document.getElementById("modeLbl");
  var diffSelEl = document.getElementById("diffSel");
  var newBtn = document.getElementById("newBtn");
  var notesBtn = document.getElementById("notesBtn");
  var msgEl = document.getElementById("msg");
  var bestEl = document.getElementById("best");

  var cellEls = [];

  function buildBoard(){
    boardEl.innerHTML = "";
    cellEls = [];
    for (var idx = 0; idx < 81; idx++){
      var r = (idx / 9) | 0, c = idx % 9;
      var cell = document.createElement("div");
      cell.className = "cell";
      if (c === 2 || c === 5) cell.classList.add("br");
      if (r === 2 || r === 5) cell.classList.add("bb");
      cell.setAttribute("data-idx", String(idx));
      (function(i){
        cell.addEventListener("click", function(){ selectCell(i); });
      })(idx);
      var notes = document.createElement("div");
      notes.className = "notes";
      for (var n = 1; n <= 9; n++){
        var sp = document.createElement("span");
        sp.setAttribute("data-n", String(n));
        notes.appendChild(sp);
      }
      cell.appendChild(notes);
      boardEl.appendChild(cell);
      cellEls.push(cell);
    }
  }

  function buildPad(){
    padEl.innerHTML = "";
    for (var n = 1; n <= 9; n++){
      var b = document.createElement("button");
      b.className = "num";
      b.textContent = String(n);
      (function(v){ b.addEventListener("click", function(){ inputNumber(v); }); })(n);
      padEl.appendChild(b);
    }
    var eraseB = document.createElement("button");
    eraseB.className = "fn";
    eraseB.textContent = "ERASE";
    eraseB.addEventListener("click", function(){ inputNumber(0); });
    padEl.appendChild(eraseB);
  }

  // ---------- timer ----------
  function fmt(sec){
    var m = Math.floor(sec / 60), s = sec % 60;
    return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
  }
  function startTimer(){
    stopTimer();
    state.startTime = Date.now() - state.elapsed * 1000;
    state.running = true;
    timerHandle = setInterval(function(){
      if (!state.running) return;
      state.elapsed = Math.floor((Date.now() - state.startTime) / 1000);
      timerEl.textContent = fmt(state.elapsed);
    }, 250);
  }
  function stopTimer(){
    state.running = false;
    if (timerHandle){ clearInterval(timerHandle); timerHandle = null; }
  }

  // ---------- best times ----------
  function loadBest(){
    try {
      var raw = localStorage.getItem(STORE_KEY);
      if (!raw) return {};
      var obj = JSON.parse(raw);
      return (obj && typeof obj === "object") ? obj : {};
    } catch (e) { return {}; }
  }
  function saveBest(obj){
    try { localStorage.setItem(STORE_KEY, JSON.stringify(obj)); } catch (e) {}
  }
  function showBest(){
    var b = loadBest();
    var t = b[state.difficulty];
    if (typeof t === "number"){
      bestEl.textContent = "Best (" + state.difficulty + "): " + fmt(t);
    } else {
      bestEl.textContent = "Best (" + state.difficulty + "): --:--";
    }
  }
  function recordBest(){
    var b = loadBest();
    var cur = b[state.difficulty];
    if (typeof cur !== "number" || state.elapsed < cur){
      b[state.difficulty] = state.elapsed;
      saveBest(b);
      return true;
    }
    return false;
  }

  // ---------- conflict detection ----------
  function conflictSet(){
    var conflicts = {};
    // rows, cols, boxes
    function check(indices){
      var seen = {};
      for (var i = 0; i < indices.length; i++){
        var v = state.values[indices[i]];
        if (v === 0) continue;
        if (seen[v] === undefined) seen[v] = [indices[i]];
        else seen[v].push(indices[i]);
      }
      for (var val in seen){
        if (seen[val].length > 1){
          for (var j = 0; j < seen[val].length; j++){
            conflicts[seen[val][j]] = true;
          }
        }
      }
    }
    var r, c, i;
    for (r = 0; r < 9; r++){
      var row = [];
      for (c = 0; c < 9; c++) row.push(r*9 + c);
      check(row);
    }
    for (c = 0; c < 9; c++){
      var col = [];
      for (r = 0; r < 9; r++) col.push(r*9 + c);
      check(col);
    }
    for (var br = 0; br < 3; br++){
      for (var bc = 0; bc < 3; bc++){
        var box = [];
        for (var rr = 0; rr < 3; rr++)
          for (var cc = 0; cc < 3; cc++)
            box.push((br*3+rr)*9 + (bc*3+cc));
        check(box);
      }
    }
    return conflicts;
  }

  // ---------- rendering ----------
  function render(){
    var conflicts = conflictSet();
    var selVal = state.selected >= 0 ? state.values[state.selected] : 0;
    var selR = state.selected >= 0 ? (state.selected / 9) | 0 : -1;
    var selC = state.selected >= 0 ? state.selected % 9 : -1;

    for (var idx = 0; idx < 81; idx++){
      var cell = cellEls[idx];
      var v = state.values[idx];
      // reset classes but keep border classes + base
      cell.classList.remove("given","conflict","selected","peer","sameval");

      if (state.given[idx]) cell.classList.add("given");

      var notesEl = cell.querySelector(".notes");
      var textNode = cell.childNodes[cell.childNodes.length - 1];
      // We store text in a dedicated text node: rebuild it
      // Clear existing value text node (first non-notes child)
      // Simpler: set cell's own text via a data span
      // Remove any prior value text nodes:
      for (var k = cell.childNodes.length - 1; k >= 0; k--){
        var ch = cell.childNodes[k];
        if (ch.nodeType === 3) cell.removeChild(ch);
      }

      if (v !== 0){
        notesEl.style.display = "none";
        cell.insertBefore(document.createTextNode(String(v)), notesEl);
      } else {
        notesEl.style.display = "";
        var set = state.notes[idx];
        var spans = notesEl.querySelectorAll("span");
        for (var s = 0; s < spans.length; s++){
          var n = s + 1;
          spans[s].textContent = (set && set[n]) ? String(n) : "";
        }
      }

      if (conflicts[idx]) cell.classList.add("conflict");

      // highlighting relative to selection
      if (state.selected >= 0 && idx !== state.selected){
        var r = (idx / 9) | 0, c = idx % 9;
        var sameBox = ((r - r%3) === (selR - selR%3)) && ((c - c%3) === (selC - selC%3));
        if (r === selR || c === selC || sameBox) cell.classList.add("peer");
        if (selVal !== 0 && v === selVal) cell.classList.add("sameval");
      }
    }

    if (state.selected >= 0){
      cellEls[state.selected].classList.add("selected");
    }
  }

  function selectCell(idx){
    state.selected = idx;
    render();
  }

  // ---------- input ----------
  function inputNumber(v){
    if (state.selected < 0 || state.solved) return;
    var idx = state.selected;
    if (state.given[idx]) return;

    if (v === 0){
      // erase
      state.values[idx] = 0;
      state.notes[idx] = {};
      render();
      return;
    }

    if (state.notesMode){
      if (state.values[idx] !== 0) state.values[idx] = 0;
      if (!state.notes[idx]) state.notes[idx] = {};
      state.notes[idx][v] = !state.notes[idx][v];
      render();
    } else {
      state.values[idx] = (state.values[idx] === v) ? 0 : v;
      state.notes[idx] = {};
      render();
      checkSolved();
    }
  }

  function checkSolved(){
    // must be fully filled
    for (var i = 0; i < 81; i++){
      if (state.values[i] === 0) return;
    }
    // must match solution (guarantees valid & correct)
    for (var j = 0; j < 81; j++){
      if (state.values[j] !== state.solution[j]) return;
    }
    // solved!
    state.solved = true;
    stopTimer();
    var isBest = recordBest();
    showBest();
    msgEl.textContent = isBest ? "Solved! New best time!" : "Solved!";
  }

  // ---------- new game ----------
  function newGame(){
    msgEl.textContent = "Generating...";
    // defer so the "Generating..." paints before the (bounded) work
    setTimeout(function(){
      try {
        var built = makePuzzle(state.difficulty);
        state.puzzle = built.puzzle.slice();
        state.solution = built.solution.slice();
        state.values = built.puzzle.slice();
        state.given = [];
        state.notes = [];
        for (var i = 0; i < 81; i++){
          state.given.push(built.puzzle[i] !== 0);
          state.notes.push({});
        }
        state.selected = -1;
        state.solved = false;
        state.elapsed = 0;
        timerEl.textContent = "00:00";
        msgEl.textContent = "";
        render();
        startTimer();
        showBest();
      } catch (e) {
        // never throw to the page; fail to a trivial fallback
        msgEl.textContent = "";
        var sol = FALLBACK_SOLUTION.slice();
        var pz = sol.slice();
        // blank out a handful of cells deterministically
        var blanks = [0,10,20,30,40,50,60,70,80];
        for (var b = 0; b < blanks.length; b++) pz[blanks[b]] = 0;
        state.puzzle = pz.slice();
        state.solution = sol.slice();
        state.values = pz.slice();
        state.given = [];
        state.notes = [];
        for (var k = 0; k < 81; k++){ state.given.push(pz[k] !== 0); state.notes.push({}); }
        state.selected = -1;
        state.solved = false;
        state.elapsed = 0;
        timerEl.textContent = "00:00";
        render();
        startTimer();
        showBest();
      }
    }, 20);
  }

  // ---------- controls ----------
  function setNotesMode(on){
    state.notesMode = on;
    notesBtn.textContent = "Notes: " + (on ? "On" : "Off");
    notesBtn.classList.toggle("toggled", on);
    modeLblEl.textContent = on ? "Notes" : "Fill";
  }

  function moveSelection(dr, dc){
    var idx = state.selected;
    if (idx < 0){ selectCell(0); return; }
    var r = (idx / 9) | 0, c = idx % 9;
    r = Math.max(0, Math.min(8, r + dr));
    c = Math.max(0, Math.min(8, c + dc));
    selectCell(r*9 + c);
  }

  function onKey(e){
    var key = e.key;
    if (key >= "1" && key <= "9"){
      inputNumber(parseInt(key, 10));
      e.preventDefault();
      return;
    }
    if (key === "0" || key === "Backspace" || key === "Delete"){
      inputNumber(0);
      e.preventDefault();
      return;
    }
    if (key === "ArrowUp"){ moveSelection(-1, 0); e.preventDefault(); return; }
    if (key === "ArrowDown"){ moveSelection(1, 0); e.preventDefault(); return; }
    if (key === "ArrowLeft"){ moveSelection(0, -1); e.preventDefault(); return; }
    if (key === "ArrowRight"){ moveSelection(0, 1); e.preventDefault(); return; }
    if (key === "n" || key === "N"){ setNotesMode(!state.notesMode); e.preventDefault(); return; }
  }

  // ---------- init ----------
  function init(){
    buildBoard();
    buildPad();

    diffSelEl.addEventListener("change", function(){
      state.difficulty = diffSelEl.value;
      diffLblEl.textContent = diffSelEl.options[diffSelEl.selectedIndex].text;
      newGame();
    });
    newBtn.addEventListener("click", function(){ newGame(); });
    notesBtn.addEventListener("click", function(){ setNotesMode(!state.notesMode); });
    document.addEventListener("keydown", onKey);

    // pause timer when tab hidden, resume on return
    document.addEventListener("visibilitychange", function(){
      if (document.hidden){
        state.running = false;
      } else if (!state.solved && state.values.length){
        state.startTime = Date.now() - state.elapsed * 1000;
        state.running = true;
      }
    });

    state.difficulty = diffSelEl.value;
    diffLblEl.textContent = diffSelEl.options[diffSelEl.selectedIndex].text;
    setNotesMode(false);
    showBest();
    newGame();
  }

  if (document.readyState === "loading"){
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();
</script>
</body>
</html>