From 44ff6231441024d16fddc2b7cb648539522b04cb Mon Sep 17 00:00:00 2001
From: Maksymilian Jopek <maks@jopek.eu>
Date: Mon, 19 Dec 2022 21:04:23 +0100
Subject: v1.0.0 Added everything that's in the spec

---
 src/App.svelte | 268 +++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 240 insertions(+), 28 deletions(-)

(limited to 'src/App.svelte')

diff --git a/src/App.svelte b/src/App.svelte
index 3a1a685..104a981 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -1,41 +1,63 @@
 <script lang="ts">
+  import { getNewCoord } from "./lib/helpers";
+
   import { readGridFromFile, save } from "./lib/Saving";
 
-  import { validate, type Coord } from "./lib/Sudoku";
+  import { solve, validate, type Coord } from "./lib/Sudoku";
 
   let selTile: HTMLDivElement | EventTarget;
+  let isImmu = false;
+  let selTileCoords: Coord = { x: -1, y: -1 };
   let fileInput: HTMLInputElement;
   const getSelTile = () => selTile as HTMLDivElement;
-  const getCoord = (): Coord => ({
-    x: parseInt(getSelTile().dataset.i),
-    y: parseInt(getSelTile().dataset.j),
-  });
   let grid = [] as string[][]; //new Array(9).fill(new Array(9).fill("."));
+  let solvedGrid = [] as string[][]; //new Array(9).fill(new Array(9).fill("."));
   let immutableTiles = [] as Coord[]; //new Array(9).fill(new Array(9).fill("."));
   const tiles = [] as HTMLDivElement[][]; //new Array(9).fill(new Array(9).fill("."));
+  const doneNums = [] as HTMLDivElement[]; //new Array(9).fill(new Array(9).fill("."));
+  const doneNumsCount = [] as number[]; //new Array(9).fill(new Array(9).fill("."));
+  let hintTime = false;
+  let eraseTime = false;
+
   for (let i = 0; i < 9; i++) {
     grid.push([]);
     tiles.push([]);
+    doneNumsCount.push(0);
     for (let j = 0; j < 9; j++) {
       grid[i].push(".");
-      tiles[i].push(null);
+      // tiles[i].push(null);
     }
   }
 
   function keyDownHandler(e: KeyboardEvent) {
     const foundImmu = immutableTiles.find(
-      (el) => el.x === getCoord().x && el.y === getCoord().y
+      (el) => el.x === selTileCoords.x && el.y === selTileCoords.y
     );
-    console.log(immutableTiles);
-    console.log(foundImmu);
-    if (foundImmu) return;
+    if (foundImmu) isImmu = true;
+
+    let key = e.key;
+    key = key === "j" ? "ArrowDown" : key;
+    key = key === "k" ? "ArrowUp" : key;
+    key = key === "h" ? "ArrowLeft" : key;
+    key = key === "l" ? "ArrowRight" : key;
+    const t = key.split("Arrow")[1]?.toLowerCase();
+    if (["right", "left", "up", "down"].includes(t) && selTile) {
+      setSelTile(null, getNewCoord(t, selTileCoords));
+    }
+    if (isImmu) return;
 
-    if (e.key === "Backspace") {
-      setSelTile(".");
+    let num: string | number = parseInt(e.key);
+    if (e.key === "Backspace" || e.key === "d" || eraseTime) {
+      setGrid(".", selTileCoords);
+      num = ".";
     }
-    const num = parseInt(e.key);
-    if (Number.isNaN(num) || selTile == null || num === 0) return;
-    setSelTile(num);
+    if (Number.isNaN(num) || selTile == null || num === 0 || eraseTime) return;
+    if (hintTime) {
+      setHint(num);
+      return;
+    }
+
+    setGrid(num.toString(), selTileCoords);
     for (const tile of tiles.flat()) {
       tile.classList.remove("err");
       tile.classList.remove("good");
@@ -45,16 +67,29 @@
       tiles[bad.x][bad.y].classList.add("err");
     }
   }
-  // function showTile(val: string) {
-  //   return val === "." ? "" : val;
-  // }
-  function setSelTile(val: string | number) {
-    grid[getCoord().x][getCoord().y] = val.toString();
-    // getSelTile().innerText = val === "." ? "" : val.toString();
+  function setSelTile(
+    nst: HTMLDivElement | EventTarget | null,
+    x: number | Coord,
+    y?: number
+  ) {
+    if (selTile) getSelTile().classList.remove("selected");
+    if (typeof x === "object") {
+      y = x.y;
+      x = x.x;
+    }
+    if (y == undefined) throw new Error("y is undefinedly!");
+
+    if (nst) selTile = nst;
+    else selTile = tiles[x][y];
+    selTileCoords = { x, y };
+    if (eraseTime) setGrid(".", selTileCoords);
+    getSelTile().classList.add("selected");
   }
 
   async function load() {
-    const maybeGrid = await readGridFromFile(fileInput.files[0]);
+    const fs = fileInput.files;
+    const maybeGrid = await readGridFromFile(fs ? fs[0] : undefined);
+    fileInput.value = "";
     if (!maybeGrid) {
       alert("Error while loading file");
       return;
@@ -66,14 +101,63 @@
         tiles[i][j].classList.add("const");
       }
     }
+    if (validate(maybeGrid).length > 0) {
+      alert("Can not load grid, not valid");
+      return;
+    }
     grid = maybeGrid;
-    fileInput.value = null;
+    solvedGrid = solve(grid);
+  }
+
+  function setGrid(val: string, ax: number | Coord, ay?: number) {
+    let x = -1,
+      y = -1;
+    if (typeof ax === "object") [x, y] = [ax.x, ax.y];
+    else if (ay != null) [x, y] = [ax, ay];
+    else throw new Error("setGrid() didnt get any good arguments");
+
+    const oldNum = grid[x][y];
+    grid[x][y] = val;
+
+    if (oldNum !== ".") doneNumsCount[parseInt(oldNum) - 1]--;
+    if (val !== ".") doneNumsCount[parseInt(val) - 1]++;
+    // else getSelTile().classList.remove("wrong", "good", "err");
+
+    if (solvedGrid.length > 0) {
+      if (grid[x][y] !== solvedGrid[x][y]) getSelTile().classList.add("wrong");
+      else getSelTile().classList.add("good");
+    }
+    // if (typeof x === "object") grid[x.x][x.y] = val;
+    // else if (y != null) grid[x][y] = val;
+    // else throw new Error("setGrid() didnt get any good arguments");
+  }
+  function setHint(num: number | string) {
+    const t = getSelTile();
+    // debugger;
+    const c = t.querySelector(`.hint-${num}`);
+    if (!c) return;
+    const cih = c.innerHTML;
+    if (cih === "") c.innerHTML = num.toString();
+    else c.innerHTML = "";
+  }
+
+  function solveGrid() {
+    if (validate(grid).length > 0) {
+      alert("Grid is not valid, can not solve");
+      return;
+    }
+    grid = solve(grid);
+    for (let i = 0; i < doneNumsCount.length; i++) doneNumsCount[i] = 9;
   }
 </script>
 
 <!-- <svelte:body on:keydown|preventDefault|stopPropagation={keyDownHandler} /> -->
 <svelte:body on:keydown={keyDownHandler} />
 <main>
+  <div class="left">
+    <input type="file" on:change={load} bind:this={fileInput} />
+    <button on:click={() => save(grid)}>Save</button>
+  </div>
   <div class="grid">
     {#each grid as row, i}
       {#each row as tile, j}
@@ -81,22 +165,74 @@
           class="tile"
           class:br={j % 3 === 2 && j !== 8}
           class:bb={i % 3 === 2 && i !== 8}
-          on:click|self={(e) => (selTile = e.target)}
+          on:click|self={(e) => setSelTile(e.target, i, j)}
           data-i={i}
           data-j={j}
           bind:this={tiles[i][j]}
           on:keypress={null}
         >
           {tile === "." ? "" : tile}
+          <span class="hint hint-1" />
+          <span class="hint hint-2" />
+          <span class="hint hint-3" />
+          <span class="hint hint-4" />
+          <span class="hint hint-5" />
+          <span class="hint hint-6" />
+          <span class="hint hint-7" />
+          <span class="hint hint-8" />
+          <span class="hint hint-9" />
         </div>
       {/each}
     {/each}
   </div>
-  <button on:click={() => save(grid)}>Save</button>
-  <input type="file" on:change={load} bind:this={fileInput} />
+  <div class="right" class:err={false}>
+    <button class="btn-solve" on:click={solveGrid}>Solve</button>
+    <div>
+      <label>
+        Hint:
+        <input type="checkbox" bind:checked={hintTime} />
+      </label>
+      <label>
+        Erase:
+        <input type="checkbox" bind:checked={eraseTime} />
+      </label>
+      <div class="small-grid">
+        {#each { length: 9 } as _, i}
+          <div bind:this={doneNums[i]} class:done={doneNumsCount[i] === 9}>
+            {i + 1}
+          </div>
+        {/each}
+      </div>
+    </div>
+  </div>
 </main>
 
 <style>
+  main {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+  }
+  @media screen and (max-width: 1144px) {
+    main {
+      flex-direction: column;
+    }
+    .left {
+      min-height: 10rem;
+      align-items: center !important;
+    }
+    .right {
+      padding-top: 2rem;
+    }
+  }
+  .left {
+    display: flex;
+    justify-content: center;
+    align-items: flex-start;
+    gap: 2rem;
+    flex-direction: column;
+    padding-right: 3rem;
+  }
   .grid {
     display: grid;
     grid-template-columns: repeat(9, 1fr);
@@ -111,6 +247,48 @@
     gap: 0;
     box-sizing: border-box;
     user-select: none;
+    position: relative;
+  }
+  .hint {
+    font-size: 0.6rem;
+    position: absolute;
+  }
+  .right {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    padding-left: 3rem;
+    gap: 2rem;
+  }
+  .right > div {
+    display: flex;
+    flex-direction: column;
+  }
+  .btn-solve {
+    background-color: #ff000000;
+    border: 3px solid white;
+    color: white;
+    font-size: 1.5rem;
+  }
+  .small-grid {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 1rem;
+    margin-top: 2rem;
+  }
+  .small-grid > div {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 2rem;
+    width: 2rem;
+    line-height: 2rem;
+    padding: 0.8rem;
+    background-color: #aaaaaa;
+  }
+  .small-grid > div.done {
+    background-color: #00aaaa;
   }
   .tile:nth-child(odd) {
     background-color: lightgrey;
@@ -124,10 +302,44 @@
   .br {
     border-right: 3px solid black;
   }
-  :global(.err) {
-    background-color: rgba(150, 0, 0, 0.6) !important;
+  .err:nth-child(n) {
+    background-color: rgba(150, 0, 0, 0.6);
   }
   :global(.const) {
     color: rebeccapurple;
   }
+  :global(.wrong) {
+    color: #dd0000;
+  }
+  :global(.good) {
+    color: #00dd00;
+  }
+  @keyframes blink-animation {
+    0% {
+      background-color: revert;
+    }
+    50% {
+      background-color: pink;
+    }
+    100% {
+      background-color: revert;
+    }
+  }
+  :global(.selected) {
+    animation: blink-animation 5s ease-in-out infinite;
+  }
+  @media print {
+    .left,
+    .right {
+      display: none;
+    }
+    .tile {
+      width: 5rem;
+      /* border: 1px solid black; */
+      print-color-adjust: exact;
+    }
+    :global(.selected) {
+      animation: none;
+    }
+  }
 </style>
-- 
cgit v1.3.1

