diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..3b462cb
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,23 @@
+node_modules
+
+# Output
+.output
+.vercel
+.netlify
+.wrangler
+/.svelte-kit
+/build
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Env
+.env
+.env.*
+!.env.example
+!.env.test
+
+# Vite
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..15dade8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,19 @@
+FROM node:18.19-alpine AS builder
+WORKDIR /app-builder
+
+COPY ./package*.json ./
+RUN npm ci
+
+COPY . .
+RUN npm run build
+
+FROM nginx:1.25.1
+WORKDIR /app
+ARG VERSION=next
+# NGINX redirect and cache control
+RUN sed -i '8 a \ \ \ \ \ \ \ \ try_files $uri $uri /index.html;' /etc/nginx/conf.d/default.conf
+# RUN sed -i "10 a \ \ \ \ \ \ \ \ location /env.js { add_header Cache-Control 'no-store'; add_header Cache-Control 'no-cache'; expires 0;}" /etc/nginx/conf.d/default.conf
+# https://pumpingco.de/blog/environment-variables-angular-docker/
+# RUN echo -n 'envsubst < /usr/share/nginx/html/env.template.js > /usr/share/nginx/html/env.js\nrm /usr/share/nginx/html/env.template.js' > /docker-entrypoint.d/100-set-env.sh && chmod +x /docker-entrypoint.d/100-set-env.sh
+
+COPY --from=builder /app-builder/build /usr/share/nginx/html
diff --git a/package-lock.json b/package-lock.json
index 31e4f76..e999dce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
+ "js-confetti": "^0.12.0",
"jsdom": "^26.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
@@ -3411,6 +3412,13 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/js-confetti": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/js-confetti/-/js-confetti-0.12.0.tgz",
+ "integrity": "sha512-1R0Akxn3Zn82pMqW65N1V2NwKkZJ75bvBN/VAb36Ya0YHwbaSiAJZVRr/19HBxH/O8x2x01UFAbYI18VqlDN6g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -3888,6 +3896,19 @@
"node": ">=8.6"
}
},
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -4115,13 +4136,15 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
diff --git a/package.json b/package.json
index 75ed387..9837e31 100644
--- a/package.json
+++ b/package.json
@@ -24,19 +24,20 @@
"@tailwindcss/vite": "^4.0.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.4",
- "eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
+ "eslint": "^9.18.0",
"globals": "^16.0.0",
+ "js-confetti": "^0.12.0",
"jsdom": "^26.0.0",
- "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
- "svelte": "^5.0.0",
+ "prettier": "^3.4.2",
"svelte-check": "^4.0.0",
+ "svelte": "^5.0.0",
"tailwindcss": "^4.0.0",
- "typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
+ "typescript": "^5.0.0",
"vite": "^6.0.0",
"vitest": "^3.0.0"
}
diff --git a/src/lib/Board.svelte b/src/lib/Board.svelte
new file mode 100644
index 0000000..f64ab48
--- /dev/null
+++ b/src/lib/Board.svelte
@@ -0,0 +1,79 @@
+
+
+
+
STATUS: {status}
+
+
+ {#each board.grid as row, i (row)}
+ {#each row as cell, j}
+ |
+ {/each}
+
+ {/each}
+ {#each board.verticalSum as s}
+
+ {/each}
+
+
+
diff --git a/src/lib/Cell.svelte b/src/lib/Cell.svelte
new file mode 100644
index 0000000..4903467
--- /dev/null
+++ b/src/lib/Cell.svelte
@@ -0,0 +1,14 @@
+
+
+
diff --git a/src/lib/GameCommands.svelte b/src/lib/GameCommands.svelte
new file mode 100644
index 0000000..60181ad
--- /dev/null
+++ b/src/lib/GameCommands.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/src/lib/SumIndicator.svelte b/src/lib/SumIndicator.svelte
new file mode 100644
index 0000000..7f5a7e1
--- /dev/null
+++ b/src/lib/SumIndicator.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/lib/ValueSelector.svelte b/src/lib/ValueSelector.svelte
new file mode 100644
index 0000000..6584f21
--- /dev/null
+++ b/src/lib/ValueSelector.svelte
@@ -0,0 +1,23 @@
+
+
+
+ {#each numbers as number (number)}
+
+ {/each}
+
+
diff --git a/src/lib/core/board.test.ts b/src/lib/core/board.test.ts
new file mode 100644
index 0000000..7292247
--- /dev/null
+++ b/src/lib/core/board.test.ts
@@ -0,0 +1,58 @@
+import '@testing-library/jest-dom/vitest';
+import { describe, expect, test } from 'vitest';
+import { Board, Difficulty } from './board';
+
+describe('Board', () => {
+ test('shape', () => {
+ const board = new Board();
+
+ expect(board.grid.length).toBe(3);
+ expect(board.grid[0].length).toBe(3);
+ expect(board.horizontalSum.length).toBe(3);
+ expect(board.verticalSum.length).toBe(3);
+ });
+
+ test('isComplete', () => {
+ const board = new Board();
+ expect(board.isComplete()).toBe(true);
+
+ // remove one number
+ board.setByPos(3);
+ expect(board.isComplete()).toBe(false);
+ });
+
+ test('isValid', () => {
+ const board = new Board();
+ expect(board.isValid()).toBe(true);
+ });
+
+ test('isValid ko', () => {
+ const board = new Board();
+ board.horizontalSum[1] = -1;
+ expect(board.isValid()).toBe(false);
+ });
+
+ test('setByPos', () => {
+ const board = new Board();
+ // set board from 9 to 1
+ for (let index = 0; index < 9; index++) {
+ board.setByPos(index, 9 - index);
+ }
+ expect(board.grid[0][0]).toBe(9);
+ expect(board.grid[0][2]).toBe(7);
+ expect(board.grid[1][0]).toBe(6);
+ expect(board.grid[1][1]).toBe(5);
+ expect(board.grid[2][2]).toBe(1);
+ });
+
+ test('prepare', () => {
+ const board = new Board();
+ board.prepare(Difficulty.Easy);
+
+ let emptyCellNb = 0;
+ for (const cell of board) {
+ if (cell === undefined) emptyCellNb++;
+ }
+ expect(emptyCellNb).toBe(9 - 3);
+ });
+});
diff --git a/src/lib/core/board.ts b/src/lib/core/board.ts
new file mode 100644
index 0000000..0a2261c
--- /dev/null
+++ b/src/lib/core/board.ts
@@ -0,0 +1,130 @@
+export enum Difficulty {
+ Easy,
+ Medium,
+ Hard,
+ Last,
+ Done
+}
+
+export type Case = number | undefined;
+
+function shuffleArray(array: Array): void {
+ for (let i = array.length - 1; i >= 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+}
+
+function getNumberToKeepFromDifficulty(difficulty: Difficulty) {
+ const rules = {
+ // 0: 8,
+ 0: 3,
+ 1: 2,
+ 2: 1,
+ 3: 8,
+ 4: 9
+ };
+ return rules[difficulty];
+}
+
+export class Board {
+ grid: Case[][];
+ verticalSum: number[];
+ horizontalSum: number[];
+ lockedCells: boolean[][];
+
+ constructor() {
+ const numbers = Array.from({ length: 9 }, (_, i) => i + 1);
+ shuffleArray(numbers);
+
+ this.grid = [];
+ this.verticalSum = new Array(3).fill(0);
+ this.horizontalSum = [];
+ this.lockedCells = [];
+
+ for (let i = 0; i < 3; i++) {
+ const row: number[] = [];
+ for (let j = 0; j < 3; j++) {
+ row[j] = numbers.pop() || 0;
+ this.verticalSum[j] += row[j];
+ }
+ this.grid.push(row);
+ this.horizontalSum[i] = row.reduce((partialSum, a) => partialSum + a, 0);
+
+ this.lockedCells[i] = [false, false, false];
+ }
+ }
+
+ [Symbol.iterator]() {
+ let index = 0;
+ return {
+ next: () => {
+ if (index < 9) {
+ const [row, col] = this.positionToRowCol(index++);
+ return { value: this.grid[row][col], done: false };
+ } else {
+ return { done: true };
+ }
+ }
+ };
+ }
+
+ isComplete(): boolean {
+ for (const cell of this) {
+ if (cell === undefined) return false;
+ }
+ return true;
+ }
+
+ isValid(): boolean {
+ for (let i = 0; i < 3; i++) {
+ const rowSum = this.grid[i].reduce((partialSum, a) => (partialSum || 0) + (a || 0), 0);
+ const colSum = (this.grid[0][i] || 0) + (this.grid[1][i] || 0) + (this.grid[2][i] || 0);
+ if (rowSum !== this.horizontalSum[i] || colSum !== this.verticalSum[i]) return false;
+ }
+ return true;
+ }
+
+ prepare(difficulty: Difficulty) {
+ const numbers = Array.from({ length: 9 }, (_, i) => i);
+ shuffleArray(numbers);
+ const numberToKeep = getNumberToKeepFromDifficulty(difficulty);
+ const posToHide = numbers.slice(numberToKeep, 9);
+
+ posToHide.forEach((pos) => {
+ this.setByPos(pos);
+ // lock visible cells
+ const [row, col] = this.positionToRowCol(pos);
+ this.lockedCells[row][col] = true;
+ });
+ }
+
+ reset() {
+ console.log('ici');
+ for (let i = 0; i < 3; i++) {
+ for (let j = 0; j < 3; j++) {
+ if (this.lockedCells[i][j]) this.grid[i][j] = undefined;
+ }
+ }
+ }
+
+ setByPos(pos: number, value: Case = undefined) {
+ if (pos < 0 || pos > 8) throw new Error('position must be between 0 and 8!');
+ if (value && (value < 1 || value > 9)) throw new Error('value must be between 1 and 9!');
+
+ const [row, col] = this.positionToRowCol(pos);
+ this.grid[row][col] = value;
+ }
+
+ positionToRowCol(position: number) {
+ return [Math.floor(position / 3), position % 3];
+ }
+
+ getPlacedNumbers() {
+ const res = [];
+ for (const pos of this) {
+ if (pos !== undefined) res.push(pos);
+ }
+ return res;
+ }
+}
diff --git a/src/lib/index.ts b/src/lib/index.ts
deleted file mode 100644
index 856f2b6..0000000
--- a/src/lib/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-// place files you want to import through the `$lib` alias in this folder.
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index cc88df0..5bf5167 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,2 +1,7 @@
-Welcome to SvelteKit
-Visit svelte.dev/docs/kit to read the documentation
+
+
+Fubuki
+
+
diff --git a/svelte.config.js b/svelte.config.js
index 64694ad..b8294b4 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -6,9 +6,10 @@ const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
-
kit: {
- adapter: adapter()
+ adapter: adapter({
+ fallback: 'index.html'
+ })
}
};