Compare commits
No commits in common. "master" and "sveltekit" have entirely different histories.
13
.eslintignore
Normal file
13
.eslintignore
Normal file
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
30
.eslintrc.cjs
Normal file
30
.eslintrc.cjs
Normal file
|
@ -0,0 +1,30 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
.idea
|
||||
.vscode
|
13
.prettierignore
Normal file
13
.prettierignore
Normal file
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
BIN
PLATZHALTER.png
BIN
PLATZHALTER.png
Binary file not shown.
Before Width: | Height: | Size: 3.9 KiB |
38
README.md
Normal file
38
README.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
267
client.js
267
client.js
|
@ -1,267 +0,0 @@
|
|||
let totalMines = 15;
|
||||
let gridWidth = 10;
|
||||
let gridHeight = 10;
|
||||
let grid, hasLost = false, hasWon = false;
|
||||
|
||||
function resetGame() {
|
||||
totalMines = parseInt(document.getElementById("tMines").value);
|
||||
gridWidth = parseInt(document.getElementById("gWidth").value);
|
||||
gridHeight = parseInt(document.getElementById("gHeight").value);
|
||||
|
||||
hasWon = false;
|
||||
hasLost = false;
|
||||
grid = startGame();
|
||||
}
|
||||
|
||||
function revealRest() {
|
||||
while (!hasWon) {
|
||||
let hasClickedOnce = false;
|
||||
for (let i = 0; i < gridHeight; i++) {
|
||||
for (let j = 0; j < gridWidth; j++) {
|
||||
if (!grid[i][j].isRevealed) {
|
||||
let current = grid[i][j].element;
|
||||
if (!grid[i][j].isFlagged && grid[i][j].isMine) {
|
||||
rightClickFunc(current);
|
||||
hasClickedOnce = true;
|
||||
break;
|
||||
} else if (!grid[i][j].isMine) {
|
||||
reveal(current);
|
||||
hasClickedOnce = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasClickedOnce) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
checkWin();
|
||||
}
|
||||
}
|
||||
|
||||
function checkWin() {
|
||||
for (let i = 0; i < gridHeight; i++) {
|
||||
for (let j = 0; j < gridWidth; j++) {
|
||||
if (!grid[i][j].isRevealed && !grid[i][j].isFlagged) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(function(){
|
||||
alert("Du hast gewonnen!!!");
|
||||
}, 500);
|
||||
hasWon = true;
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
for (let i = 0; i < gridHeight; i++) {
|
||||
for (let j = 0; j < gridWidth; j++) {
|
||||
if (!grid[i][j].isFlagged) {
|
||||
grid[i][j].isRevealed = true;
|
||||
if (grid[i][j].isMine) {
|
||||
grid[i][j].element.src = "MINE.png";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(function(){
|
||||
alert("Du hast verloren!!!");
|
||||
}, 500);
|
||||
hasLost = true;
|
||||
}
|
||||
|
||||
function reveal(eventTarget) {
|
||||
let et = eventTarget.parentElement;
|
||||
if (et && et.value) {
|
||||
let coords = et.value.split(' ');
|
||||
let clickY = parseInt(coords[0]);
|
||||
let clickX = parseInt(coords[1]);
|
||||
if (!grid[clickY][clickX].isFlagged) {
|
||||
let number, pDiv = document.createElement("div");
|
||||
let image = document.createElement("img");
|
||||
grid[clickY][clickX].isRevealed = true;
|
||||
if (grid[clickY][clickX].isMine) {
|
||||
image.src = "MINE.png";
|
||||
endGame();
|
||||
} else {
|
||||
image.src = "BLANK.png";
|
||||
let toBeRevealed = Array(), bombsNearby = 0;
|
||||
for (let i = -1; i < 2; i++) {
|
||||
for (let j = -1; j < 2; j++) {
|
||||
if (!(i == 0 && j == 0)) {
|
||||
const nY = clickY + i;
|
||||
const nX = clickX + j;
|
||||
if ((nX >= 0 && nX < gridWidth) && (nY >= 0 && nY < gridHeight)) {
|
||||
let nE = grid[nY][nX];
|
||||
if (!nE.isMine) {
|
||||
if (!nE.isRevealed) {
|
||||
toBeRevealed.push(nE.element);
|
||||
}
|
||||
} else {
|
||||
bombsNearby++;
|
||||
grid[clickY][clickX].isNumber = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bombsNearby == 0) {
|
||||
toBeRevealed.forEach(e=>{
|
||||
reveal(e);
|
||||
});
|
||||
} else {
|
||||
number = document.createElement("a");
|
||||
number.innerText = bombsNearby;
|
||||
}
|
||||
pDiv.appendChild(image);
|
||||
if (number) {
|
||||
pDiv.appendChild(number);
|
||||
}
|
||||
pDiv.ondblclick = doubleClickFunc;
|
||||
et.replaceChildren(pDiv);
|
||||
grid[clickY][clickX].element = image;
|
||||
}
|
||||
if (!hasLost) {
|
||||
checkWin();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function doubleClickFunc(event) {
|
||||
if (!hasLost) {
|
||||
let et = event.target.parentElement.parentElement;
|
||||
if (et && et.value) {
|
||||
let coords = et.value.split(' ');
|
||||
let clickY = parseInt(coords[0]);
|
||||
let clickX = parseInt(coords[1]);
|
||||
if (grid[clickY][clickX].isNumber) {
|
||||
let toBeRevealed = Array();
|
||||
for (let i = -1; i < 2; i++) {
|
||||
for (let j = -1; j < 2; j++) {
|
||||
if (!(i == 0 && j == 0)) {
|
||||
const nY = clickY + i;
|
||||
const nX = clickX + j;
|
||||
if ((nX >= 0 && nX < gridWidth) && (nY >= 0 && nY < gridHeight)) {
|
||||
let nE = grid[nY][nX];
|
||||
if (nE.isMine && !nE.isFlagged) {
|
||||
endGame();
|
||||
break;
|
||||
} else {
|
||||
if (!nE.isRevealed) {
|
||||
toBeRevealed.push(nE.element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasLost) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasLost) {
|
||||
if (!checkWin()) {
|
||||
toBeRevealed.forEach(e=>{
|
||||
reveal(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setFlag(eventTarget) {
|
||||
let et = eventTarget.parentElement;
|
||||
if (et && et.value) {
|
||||
let coords = et.value.split(' ');
|
||||
let clickY = parseInt(coords[0]);
|
||||
let clickX = parseInt(coords[1]);
|
||||
if (!grid[clickY][clickX].isRevealed) {
|
||||
let image = grid[clickY][clickX].element;
|
||||
if (!grid[clickY][clickX].isFlagged) {
|
||||
image.src = "FLAG.png";
|
||||
et.replaceChildren(image);
|
||||
} else {
|
||||
image.src = "PLATZHALTER.png";
|
||||
et.replaceChildren(image);
|
||||
}
|
||||
grid[clickY][clickX].isFlagged = !grid[clickY][clickX].isFlagged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rightClickFunc(event) {
|
||||
if (event.target) {
|
||||
event.preventDefault();
|
||||
if (!hasLost) {
|
||||
setFlag(event.target);
|
||||
}
|
||||
} else {
|
||||
setFlag(event);
|
||||
}
|
||||
}
|
||||
|
||||
function onClickFunc(event) {
|
||||
if (!hasLost) {
|
||||
reveal(event.target);
|
||||
}
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
let g = Array();
|
||||
for (let i = 0; i < gridHeight; i++) {
|
||||
g.push(Array(gridWidth));
|
||||
}
|
||||
let t = document.getElementById("Game");
|
||||
t.innerHTML = "";
|
||||
for (let i = 0; i < gridHeight; i++) {
|
||||
let r = document.createElement("tr");
|
||||
for (let j = 0; j < gridWidth; j++) {
|
||||
let d = document.createElement("td");
|
||||
let image = document.createElement("img");
|
||||
image.src = "PLATZHALTER.png";
|
||||
image.onclick = onClickFunc;
|
||||
image.oncontextmenu = rightClickFunc;
|
||||
d.appendChild(image);
|
||||
d.value = i + " " + j;
|
||||
r.appendChild(d);
|
||||
g[i][j] = new Block(image);
|
||||
}
|
||||
t.appendChild(r);
|
||||
}
|
||||
let mC = 0;
|
||||
while (mC < totalMines) {
|
||||
let mX = Math.floor(Math.random()*gridWidth);
|
||||
let mY = Math.floor(Math.random()*gridHeight);
|
||||
if (!g[mY][mX].isMine) {
|
||||
g[mY][mX].isMine=true;
|
||||
mC++;
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
function init() {
|
||||
grid = startGame();
|
||||
document.getElementById("gWidth").oninput = function() {
|
||||
let gHValue = document.getElementById("gHeight").value;
|
||||
let value = Math.floor(parseInt(this.value) * parseInt(gHValue) * 0.1);
|
||||
document.getElementById("tMines").max = value;
|
||||
};
|
||||
document.getElementById("gHeight").oninput = function() {
|
||||
let gWValue = document.getElementById("gWidth").value;
|
||||
let value = Math.floor(parseInt(this.value) * parseInt(gWValue) * 0.1) ;
|
||||
document.getElementById("tMines").max = value+"";
|
||||
};
|
||||
}
|
||||
|
||||
class Block {
|
||||
constructor(e_) {
|
||||
this.element = e_;
|
||||
this.isMine = false;
|
||||
this.isRevealed = false;
|
||||
this.isFlagged = false;
|
||||
this.isNumber = false;
|
||||
}
|
||||
}
|
BIN
data/sessions.db
Normal file
BIN
data/sessions.db
Normal file
Binary file not shown.
37
index.html
37
index.html
|
@ -1,37 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="client.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: arial;
|
||||
}
|
||||
div {
|
||||
display: grid;
|
||||
}
|
||||
div > * {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
}
|
||||
img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
z-index: 0;
|
||||
|
||||
}
|
||||
a {
|
||||
z-index: 1;
|
||||
margin: auto;
|
||||
padding: auto;
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body onload="init();">
|
||||
<table id="Game"></table>
|
||||
Minen: <input id="tMines" type="number" value="15" min="1" max="100"><br>
|
||||
Breite: <input id="gWidth" type="number" value="10" min="5" max="100"><br>
|
||||
Höhe: <input id="gHeight" type="number" value="10" min="5" max="100"><br>
|
||||
<button onclick="resetGame();">Reset Game</button>
|
||||
</body>
|
||||
</html>
|
7692
package-lock.json
generated
Normal file
7692
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "minesweeper",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@types/better-sqlite3": "^7.6.4",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"postcss": "^8.4.27",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2",
|
||||
"vitest": "^0.32.2"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.5.0",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
12
playwright.config.ts
Normal file
12
playwright.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
export default config;
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
3
src/app.css
Normal file
3
src/app.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
12
src/app.d.ts
vendored
Normal file
12
src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
src/app.html
Normal file
12
src/app.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
27
src/global.d.ts
vendored
Normal file
27
src/global.d.ts
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Interface to keep track of session data
|
||||
interface Session {
|
||||
id: string;
|
||||
matrix?: boolean[][];
|
||||
}
|
||||
|
||||
// Interface to store raw session in
|
||||
interface RawSession {
|
||||
id: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
// Interface to keep track of an individual cell
|
||||
interface Cell {
|
||||
x: number;
|
||||
y: number;
|
||||
mine: boolean;
|
||||
neighbors: number;
|
||||
}
|
||||
|
||||
// Interface to mimic a raw response object from http-requests
|
||||
interface RawResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
cells?: cell[];
|
||||
done?: boolean;
|
||||
}
|
17
src/hooks.server.ts
Normal file
17
src/hooks.server.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { Handle } from '@sveltejs/kit';
|
||||
import { createSession, getSession } from '$lib/server/database';
|
||||
|
||||
// Hook to check if session is being kept track of
|
||||
export const handle = (async ({ event, resolve }) => {
|
||||
const session_id: string | undefined = event.cookies.get('session_id');
|
||||
let session =
|
||||
session_id !== undefined ? (getSession(session_id) as Session | undefined) : undefined;
|
||||
// If not, create new one and save in cookie
|
||||
if (session === undefined) {
|
||||
session = createSession();
|
||||
event.cookies.set('session_id', session.id);
|
||||
}
|
||||
|
||||
// Resolve actual page (get-request)
|
||||
return await resolve(event);
|
||||
}) satisfies Handle;
|
7
src/index.test.ts
Normal file
7
src/index.test.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
32
src/lib/client/message.ts
Normal file
32
src/lib/client/message.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Display message on a small window with provided background color
|
||||
// Color format: css-style; color name or rgb(x, x, x)
|
||||
export function displayMessage(
|
||||
elements: HTMLElement[],
|
||||
message: string,
|
||||
bgColor: string,
|
||||
txtColor: string,
|
||||
optFunc?: Function
|
||||
) {
|
||||
elements[0].classList.replace('hidden', 'flex');
|
||||
elements[1].classList.replace('hidden', 'flex');
|
||||
elements[2].style.backgroundColor = bgColor;
|
||||
elements[2].style.color = txtColor;
|
||||
elements[2].textContent = message;
|
||||
|
||||
elements[1].onclick = () => {
|
||||
elements[0].classList.replace('flex', 'hidden');
|
||||
elements[1].classList.replace('flex', 'hidden');
|
||||
if (optFunc !== undefined && optFunc !== null) optFunc();
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export function getElements(): HTMLElement[] {
|
||||
const elements: (HTMLElement|null)[] = [];
|
||||
elements[0] = document.getElementById('EMSG-background');
|
||||
elements[1] = document.getElementById('EMSG-container');
|
||||
elements[2] = document.getElementById('EMSG-text');
|
||||
const filtered = elements.filter(e => e !== null) as HTMLElement[];
|
||||
if (filtered.length !== 3) throw new Error("Could not find the desired Event-Message elements.");
|
||||
return filtered;
|
||||
}
|
45
src/lib/server/database.ts
Normal file
45
src/lib/server/database.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import Database from 'better-sqlite3';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// Connect to DB and create table if necessary
|
||||
export const db = new Database('data/sessions.db');
|
||||
db.exec('CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY NOT NULL, data TEXT)');
|
||||
|
||||
// Create session in DB and return it
|
||||
export function createSession(): Session {
|
||||
const sessionID = uuidv4();
|
||||
const session: Session = { id: sessionID };
|
||||
db.prepare('INSERT INTO sessions (id, data) VALUES (?, ?)').run(sessionID, null);
|
||||
return session;
|
||||
}
|
||||
|
||||
// Check if session exists in DB
|
||||
export function checkSession(session: Session): boolean {
|
||||
const id: string = session.id;
|
||||
const result: Session | undefined = getSession(id);
|
||||
return result !== undefined;
|
||||
}
|
||||
|
||||
// Parse raw session to real session
|
||||
export function parseSession(session: RawSession): Session {
|
||||
return { id: session.id, matrix: JSON.parse(session.data) } as Session;
|
||||
}
|
||||
|
||||
// Get session from DB by id
|
||||
export function getSession(id: string): Session | undefined {
|
||||
const raw = db.prepare('SELECT id, data FROM sessions WHERE id = ?').get(id) as
|
||||
| RawSession
|
||||
| undefined;
|
||||
if (raw === undefined) return;
|
||||
|
||||
return parseSession(raw);
|
||||
}
|
||||
|
||||
// Save session into DB
|
||||
export function saveSession(session: Session) {
|
||||
if (session.matrix === null || session.matrix === undefined) return;
|
||||
db.prepare('UPDATE sessions SET data = ? WHERE id = ?').run(
|
||||
JSON.stringify(session.matrix),
|
||||
session.id
|
||||
);
|
||||
}
|
48
src/lib/server/functions.ts
Normal file
48
src/lib/server/functions.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
// Generate new boolean matrix
|
||||
export function generateMatrix(width: number, height: number): boolean[][] {
|
||||
const grid: boolean[][] = [];
|
||||
|
||||
// Add rows to matrix
|
||||
for (let j = 0; j < height; j++) {
|
||||
// Create row, add cells
|
||||
const next: boolean[] = [];
|
||||
for (let i = 0; i < width; i++) next.push(false);
|
||||
// Push row onto matrix
|
||||
grid.push(next);
|
||||
}
|
||||
|
||||
// Generate exactly 10% as many mines as there are cells
|
||||
let count = 0;
|
||||
const max: number = width * height * 0.1;
|
||||
while (count < max) {
|
||||
// Try setting random position to mine
|
||||
const x: number = Math.floor(Math.random() * width);
|
||||
const y: number = Math.floor(Math.random() * height);
|
||||
|
||||
if (grid[y][x]) continue;
|
||||
grid[y][x] = true;
|
||||
// Keep track of mine count
|
||||
count++;
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
// Count mines adjacent to given cell
|
||||
export function countAdjacent(cell: Cell, matrix: boolean[][]): number {
|
||||
let count = 0;
|
||||
for (let dy = -1; dy < 2; dy++)
|
||||
for (let dx = -1; dx < 2; dx++) {
|
||||
// Ignore self
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
const nx = cell.x + dx,
|
||||
ny = cell.y + dy;
|
||||
// Ignore out of bounds
|
||||
if (ny < 0 || ny >= matrix.length) continue;
|
||||
if (nx < 0 || nx >= matrix[ny].length) continue;
|
||||
// Add mine to count
|
||||
if (matrix[ny][nx]) count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
369
src/routes/(app)/+page.svelte
Normal file
369
src/routes/(app)/+page.svelte
Normal file
|
@ -0,0 +1,369 @@
|
|||
<script lang="ts">
|
||||
import { displayMessage, getElements } from '$lib/client/message';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// Keep track of dimensions and sizes
|
||||
let width = 10,
|
||||
height = 10;
|
||||
let cellWidth: number,
|
||||
cellHeight: number,
|
||||
fontSize = 10;
|
||||
|
||||
// Function to calculate font size based on cell dimensions
|
||||
function calculateFontSize() {
|
||||
const cell = document.querySelector('td') as HTMLTableCellElement | null;
|
||||
if (cell === null) return;
|
||||
fontSize = cell.offsetHeight / 2;
|
||||
const cOW = cell.offsetWidth / 2;
|
||||
// Choose whichever is smaller
|
||||
if (cOW < fontSize) fontSize = cOW;
|
||||
console.log(`Font size: ${fontSize}px.`);
|
||||
}
|
||||
|
||||
// Keep track of indecies in matrix and cells that have been revealed
|
||||
let changed: HTMLTableCellElement[] = [];
|
||||
let matrix: number[][] = [];
|
||||
// Function to update matrix due to resize or reset
|
||||
function updateMatrix(generate: boolean) {
|
||||
// Save matrix dimensions
|
||||
window.localStorage.setItem('width', width.toString());
|
||||
window.localStorage.setItem('height', height.toString());
|
||||
// Calculate percentual cell dimensions
|
||||
cellWidth = 100 / width;
|
||||
cellHeight = 100 / height;
|
||||
// Try calculating font-size
|
||||
calculateFontSize();
|
||||
|
||||
// Prepare URL search params for new dimensions
|
||||
if (generate) {
|
||||
const params: URLSearchParams = new URLSearchParams();
|
||||
params.set('w', width.toString());
|
||||
params.set('h', height.toString());
|
||||
// Send get-request to set new matrix
|
||||
fetch('/generate?' + params.toString())
|
||||
// Parse response to JSON
|
||||
.then((response: Response) => response.json())
|
||||
// Error handling in case response is negative
|
||||
.then((response: RawResponse) => {
|
||||
const { success, message } = response;
|
||||
if (!success) throw new Error(`Couldn't generate new matrix!\n${message}`);
|
||||
console.log(message);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset matrix
|
||||
matrix = [];
|
||||
for (let j = 0; j < height; j++) {
|
||||
// Create row, add cells
|
||||
let row: number[] = [];
|
||||
for (let i = 0; i < width; i++) row.push(i + j * width);
|
||||
// Push row onto matrix
|
||||
matrix.push(row);
|
||||
}
|
||||
// If cells were changed, reset them and empty array
|
||||
if (changed.length === 0) return;
|
||||
changed.forEach((cell) => {
|
||||
if (!cell.classList.contains('cursor-pointer'))
|
||||
cell.classList.add('cursor-pointer');
|
||||
cell.removeAttribute('data-clicked');
|
||||
cell.removeAttribute('data-marked');
|
||||
cell.style.backgroundColor = '';
|
||||
cell.textContent = '';
|
||||
});
|
||||
changed = [];
|
||||
}
|
||||
|
||||
// Get last matrix dimensions
|
||||
function getDimensions(): boolean {
|
||||
const lw: string | null = window.localStorage.getItem('width');
|
||||
const lh: string | null = window.localStorage.getItem('height');
|
||||
if (lw === null || lh === null) return false;
|
||||
// Try parsing the dimensions
|
||||
width = parseInt(lw);
|
||||
height = parseInt(lh);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if matrix was generated yet
|
||||
async function requestMatrix(): Promise<boolean> {
|
||||
const response: Response = await fetch('request');
|
||||
if (response.status !== 200) return false;
|
||||
const value: boolean = await response.json();
|
||||
return value;
|
||||
}
|
||||
|
||||
// Try not to 'eagerly' call http-requests
|
||||
onMount(async () => {
|
||||
const lcCheck: boolean = await getDimensions();
|
||||
const dbCheck: boolean = await requestMatrix();
|
||||
// Update matrix with dimensions
|
||||
await updateMatrix(!(lcCheck && dbCheck));
|
||||
// Artificial delay to calculate font size after render
|
||||
// (i think that's the problem at least :^P)
|
||||
await calculateFontSize();
|
||||
});
|
||||
|
||||
// Display the reaveling of this cell
|
||||
function displayChange(cell: HTMLTableCellElement, color: string, count: number) {
|
||||
if (cell.dataset.marked === 'true') return;
|
||||
if (count !== 0) cell.textContent = count.toString();
|
||||
cell.classList.remove('cursor-pointer');
|
||||
cell.style.backgroundColor = color;
|
||||
cell.dataset.clicked = 'true';
|
||||
changed.push(cell);
|
||||
}
|
||||
|
||||
// Calculate coordinates from index
|
||||
function toCoords(index: number) {
|
||||
return {
|
||||
x: index % width,
|
||||
y: Math.floor(index / width)
|
||||
} as { x: number; y: number };
|
||||
}
|
||||
|
||||
// Call reaveal function on server, display reveal on client
|
||||
function lmbclick(event: MouseEvent) {
|
||||
// Get cell from event
|
||||
const cell = event.target as HTMLTableCellElement;
|
||||
|
||||
// Try reaveling the element
|
||||
reveal(cell);
|
||||
}
|
||||
|
||||
function NodeListToArray(list: NodeListOf<Element>): Element[] {
|
||||
return Array.prototype.slice.call(list);
|
||||
}
|
||||
|
||||
async function checkWinCondition() {
|
||||
// Get all HTML Table Cell Elements
|
||||
const cells = document.querySelectorAll('td') as NodeListOf<HTMLTableCellElement>;
|
||||
const array = NodeListToArray(cells) as HTMLTableCellElement[];
|
||||
|
||||
// Convert all HTML Table Cell Elements to Cells
|
||||
const mines = array.filter(e => e.dataset.marked).map(e => {
|
||||
// Get id from cell
|
||||
if (e.dataset.id === undefined) return null;
|
||||
const index: number = parseInt(e.dataset.id);
|
||||
// Calculate coordinates from index
|
||||
const { x, y } = toCoords(index);
|
||||
return { x, y } as Cell;
|
||||
}).filter(e => e !== null) as Cell[];
|
||||
|
||||
// Prepare URL serach param for cell array
|
||||
const params: URLSearchParams = new URLSearchParams();
|
||||
params.set('mines', JSON.stringify(mines));
|
||||
|
||||
// Send get-request to verify marked as mines
|
||||
const response: Response = await fetch('/checkMarked?' + params.toString());
|
||||
// Parse response to JSON
|
||||
const data: RawResponse = await response.json();
|
||||
|
||||
// Get status for error handling and cells in case it works
|
||||
const { success, message, done } = data;
|
||||
if (done === undefined || !success)
|
||||
throw new Error(`Couldn't verify marked cells!\n${message}`);
|
||||
|
||||
console.log(message);
|
||||
// If all marked cells have been verified as mines
|
||||
if (done) {
|
||||
// Display an event message, reset matrix on click
|
||||
const elements = getElements();
|
||||
const onClickFunc = () => { updateMatrix(false); };
|
||||
displayMessage(elements, 'You won!', 'white', 'black', onClickFunc);
|
||||
}
|
||||
}
|
||||
|
||||
async function reveal(cell: HTMLTableCellElement) {
|
||||
if (cell.dataset.clicked) return;
|
||||
// Get id from cell
|
||||
if (cell.dataset.id === undefined) return;
|
||||
const index: number = parseInt(cell.dataset.id);
|
||||
// Calculate coordinates from index
|
||||
const { x, y } = toCoords(index);
|
||||
|
||||
// Prepare URL serach params for coordinates
|
||||
const params: URLSearchParams = new URLSearchParams();
|
||||
params.set('x', x.toString());
|
||||
params.set('y', y.toString());
|
||||
// Send get-request to try reveal cell
|
||||
const response: Response = await fetch('/reveal?' + params.toString());
|
||||
// Parse response to JSON
|
||||
const data: RawResponse = await response.json();
|
||||
|
||||
// Get status for error handling and cells in case it works
|
||||
const { success, message, cells } = data;
|
||||
if (cells === undefined || !success)
|
||||
throw new Error(`Couldn't reveal next cell!\n${message}`);
|
||||
|
||||
console.log(message);
|
||||
// Hit a mine, display the reveal
|
||||
if (cells.length === 1 && cells[0].mine) {
|
||||
displayChange(cell, 'red', 0);
|
||||
// Display an event message, reset matrix on click
|
||||
const elements = getElements();
|
||||
const onClickFunc = () => { updateMatrix(false); };
|
||||
displayMessage(elements, 'You lost!', 'white', 'black', onClickFunc);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, diplay all revealed cells in array
|
||||
for (const current of cells) {
|
||||
// Deconstruct cell contents and calculate index
|
||||
const { x, y, neighbors } = current as Cell,
|
||||
index = x + y * width;
|
||||
// Get cell on page by index
|
||||
const element = document.querySelector(
|
||||
`td[data-id='${index}']`
|
||||
) as HTMLTableCellElement | null;
|
||||
if (element === null) continue;
|
||||
// Display the reveal
|
||||
displayChange(element, 'lightgrey', neighbors);
|
||||
}
|
||||
|
||||
// Check whether winning condition is met
|
||||
checkWinCondition();
|
||||
}
|
||||
|
||||
// Handle reveal logic on right click MouseEvent
|
||||
function mark(event: MouseEvent) {
|
||||
// Get cell from event
|
||||
const cell = event.target as HTMLTableCellElement;
|
||||
|
||||
if (cell.dataset.marked) {
|
||||
console.log('Removed mark from cell.');
|
||||
// Remove mark from cell
|
||||
cell.style.backgroundColor = '';
|
||||
cell.removeAttribute('data-marked');
|
||||
cell.removeAttribute('data-clicked');
|
||||
|
||||
// Remove cell from auto-reset
|
||||
const index: number = changed.indexOf(cell);
|
||||
changed.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cell.dataset.clicked) return;
|
||||
console.log('Marked as mine.');
|
||||
// Mark cell as mine
|
||||
cell.style.backgroundColor = 'rgb(50,50,50)';
|
||||
cell.dataset.clicked = 'true';
|
||||
cell.dataset.marked = 'true';
|
||||
|
||||
// Remember cell for auto-reset
|
||||
changed.push(cell);
|
||||
|
||||
// Check whether winning condition is met
|
||||
checkWinCondition();
|
||||
}
|
||||
|
||||
// Handle reveal logic on double click MouseEvent
|
||||
function dblclick(event: MouseEvent) {
|
||||
// Get cell from event
|
||||
const cell = event.target as HTMLTableCellElement;
|
||||
if (!cell.dataset.clicked || cell.dataset.marked) return;
|
||||
|
||||
// Get id from cell
|
||||
if (cell.dataset.id === undefined) return;
|
||||
const index: number = parseInt(cell.dataset.id);
|
||||
// Calculate coordinates from index
|
||||
const { x, y } = toCoords(index);
|
||||
|
||||
// Reveal cells around the one that was clicked
|
||||
console.log('Revealing adjacent cells . . . ');
|
||||
for (let dy = -1; dy < 2; dy++)
|
||||
for (let dx = -1; dx < 2; dx++) {
|
||||
// Ignore self
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
const nx = x + dx,
|
||||
ny = y + dy;
|
||||
// Ignore out of bounds
|
||||
if (ny < 0 || ny >= matrix.length) continue;
|
||||
if (nx < 0 || nx >= matrix[ny].length) continue;
|
||||
|
||||
// Get cell on page by index
|
||||
const element = document.querySelector(
|
||||
`td[data-id='${nx + ny * width}']`
|
||||
) as HTMLTableCellElement | null;
|
||||
if (element === null) continue;
|
||||
// Try revealing the element
|
||||
reveal(element);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table-container min-h-screen">
|
||||
<!-- Matrix for game -->
|
||||
<table class="table-fixed" style="font-size: {fontSize}px">
|
||||
{#each matrix as row}
|
||||
<tr>
|
||||
{#each row as cell}
|
||||
<td
|
||||
data-id={cell}
|
||||
style="width: {cellWidth}%; height: {cellHeight}%"
|
||||
class="border border-black bg-gray-400 text-center cell cursor-pointer"
|
||||
on:click|preventDefault={(event) => lmbclick(event)}
|
||||
on:dblclick|preventDefault={(event) => dblclick(event)}
|
||||
on:contextmenu|preventDefault={(event) => mark(event)}
|
||||
/>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
<div class="p-4 bg-gray-100 flex justify-center items-center">
|
||||
<!-- Width setting -->
|
||||
<div class="mr-4">
|
||||
<label for="width" class="mr-2">Width:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="width"
|
||||
min="2"
|
||||
max="50"
|
||||
bind:value={width}
|
||||
on:input={() => {
|
||||
updateMatrix(true);
|
||||
}}
|
||||
class="border rounded px-2 py-1 w-20"
|
||||
/>
|
||||
</div>
|
||||
<!-- Height setting -->
|
||||
<div class="mr-4">
|
||||
<label for="height" class="mr-2">Height:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="height"
|
||||
min="2"
|
||||
max="25"
|
||||
bind:value={height}
|
||||
on:input={() => {
|
||||
updateMatrix(true);
|
||||
}}
|
||||
class="border rounded px-2 py-1 w-20"
|
||||
/>
|
||||
</div>
|
||||
<!-- Matrix reset -->
|
||||
<div>
|
||||
<input
|
||||
type="button"
|
||||
id="reset"
|
||||
value="Reset Matrix"
|
||||
on:click={() => {
|
||||
updateMatrix(true);
|
||||
}}
|
||||
class="bg-white hover:bg-gray-200 hover:border-black border rounded px-2 py-1 w-40 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional CSS for details -->
|
||||
<style lang="postcss">
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
table {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
60
src/routes/(requests)/checkMarked/+server.ts
Normal file
60
src/routes/(requests)/checkMarked/+server.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { parse } from 'cookie';
|
||||
import { getSession, saveSession } from '$lib/server/database';
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { generateMatrix } from '$lib/server/functions';
|
||||
|
||||
// http-get-request to handle generating a new matrix
|
||||
export const GET = (async ({ request }) => {
|
||||
// Assume default response data
|
||||
const data: RawResponse = { success: false, message: 'Invalid input or session not found.', done: false };
|
||||
|
||||
// Try retrieving the cookies
|
||||
const raw = request.headers.get('cookie');
|
||||
if (raw == null) return json(data);
|
||||
const cookies = parse(raw);
|
||||
|
||||
// Try retrieving session id from cookie
|
||||
const session_id: string | undefined = cookies['session_id'];
|
||||
if (session_id === undefined) return json(data);
|
||||
const session = getSession(session_id) as Session | undefined;
|
||||
if (session === undefined) return json(data);
|
||||
// Return if matrix has not yet been generated
|
||||
if (session.matrix === null || session.matrix === undefined) return json(data);
|
||||
|
||||
// Get URL search parameters for marked mines
|
||||
const url = new URL(request.url);
|
||||
const params = url.searchParams;
|
||||
const strCheck = params.get('mines');
|
||||
if (strCheck === null) return json(data);
|
||||
const mines = JSON.parse(strCheck) as Cell[];
|
||||
|
||||
// Count and verify marked mines
|
||||
let count = 0, isValid = true;
|
||||
for (const cell of mines) {
|
||||
const { x, y } = cell;
|
||||
// Provided cell is not a mine.
|
||||
if (!session.matrix[y][x]) {
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
const h = session.matrix.length;
|
||||
const w = session.matrix[0].length;
|
||||
// All mines have been marked
|
||||
if (isValid && count === w * h * .1) {
|
||||
data.done = true;
|
||||
data.message = 'You have successfully marked all mines!';
|
||||
// Generate and save matrix
|
||||
session.matrix = generateMatrix(w, h);
|
||||
saveSession(session);
|
||||
// Some mines remain on matrix
|
||||
} else
|
||||
data.message = 'There are still some mines left!';
|
||||
|
||||
// Successfully parsed inputs
|
||||
data.success = true;
|
||||
|
||||
return json(data);
|
||||
}) satisfies RequestHandler;
|
42
src/routes/(requests)/generate/+server.ts
Normal file
42
src/routes/(requests)/generate/+server.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { parse } from 'cookie';
|
||||
import { generateMatrix } from '$lib/server/functions';
|
||||
import { getSession, saveSession } from '$lib/server/database';
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
// http-get-request to handle generating a new matrix
|
||||
export const GET = (async ({ request }) => {
|
||||
// Assume default response data
|
||||
const data: RawResponse = { success: false, message: 'Invalid input or session not found.' };
|
||||
|
||||
// Try retrieving the cookies
|
||||
const raw = request.headers.get('cookie');
|
||||
if (raw == null) return json(data);
|
||||
const cookies = parse(raw);
|
||||
|
||||
// Try retrieving session id from cookie
|
||||
const session_id: string | undefined = cookies['session_id'];
|
||||
if (session_id === undefined) return json(data);
|
||||
const session = getSession(session_id) as Session | undefined;
|
||||
if (session === undefined) return json(data);
|
||||
|
||||
// Get URL search parameters for dimensions
|
||||
const url = new URL(request.url);
|
||||
const params = url.searchParams;
|
||||
const dimensions = { w: -1, h: -1 };
|
||||
const w = params.get('w'),
|
||||
h = params.get('h');
|
||||
// Try parsing parameters to integers
|
||||
if (w != null) dimensions.w = parseInt(w);
|
||||
if (h != null) dimensions.h = parseInt(h);
|
||||
// Return if dimensions are invalid
|
||||
if (dimensions.w <= 1 || dimensions.h <= 1) return json(data);
|
||||
|
||||
// Successfully parsed inputs
|
||||
data.success = true;
|
||||
data.message = 'Generated new matrix.';
|
||||
// Generate and save matrix
|
||||
session.matrix = generateMatrix(dimensions.w, dimensions.h);
|
||||
saveSession(session);
|
||||
|
||||
return json(data);
|
||||
}) satisfies RequestHandler;
|
23
src/routes/(requests)/request/+server.ts
Normal file
23
src/routes/(requests)/request/+server.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { parse } from 'cookie';
|
||||
import { getSession } from '$lib/server/database';
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
// http-get-request to check if a session is valid
|
||||
export const GET = (async ({ request }) => {
|
||||
let response = false;
|
||||
|
||||
// Try retrieving the cookies
|
||||
const raw = request.headers.get('cookie');
|
||||
if (raw == null) return json(response);
|
||||
const cookies = parse(raw);
|
||||
|
||||
// Try retrieving session id from cookie
|
||||
const session_id: string | undefined = cookies['session_id'];
|
||||
if (session_id === undefined) return json(response);
|
||||
const session = getSession(session_id) as Session | undefined;
|
||||
if (session === undefined) return json(response);
|
||||
// Return if matrix has not yet been generated
|
||||
response = !(session.matrix === null || session.matrix === undefined);
|
||||
|
||||
return json(response);
|
||||
}) satisfies RequestHandler;
|
122
src/routes/(requests)/reveal/+server.ts
Normal file
122
src/routes/(requests)/reveal/+server.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
import { parse } from 'cookie';
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { getSession, saveSession } from '$lib/server/database';
|
||||
import { countAdjacent, generateMatrix } from '$lib/server/functions';
|
||||
|
||||
// http-get-request to handle revealing a cell
|
||||
export const GET = (async ({ request }) => {
|
||||
// Assume default response data
|
||||
const data: RawResponse = { success: false, message: 'Invalid input or session not found.' };
|
||||
|
||||
// Try retrieving the cookies
|
||||
const raw = request.headers.get('cookie');
|
||||
if (raw == null) return json(data);
|
||||
const cookies = parse(raw);
|
||||
|
||||
// Try retrieving session id from cookie
|
||||
const session_id: string | undefined = cookies['session_id'];
|
||||
if (session_id === undefined) return json(data);
|
||||
const session = getSession(session_id) as Session | undefined;
|
||||
if (session === undefined) return json(data);
|
||||
// Return if matrix has not yet been generated
|
||||
if (session.matrix === null || session.matrix === undefined) return json(data);
|
||||
|
||||
// Get URL search parameters for coordinates
|
||||
const url = new URL(request.url);
|
||||
const params = url.searchParams;
|
||||
const coords = { x: -1, y: -1 };
|
||||
const raws = {
|
||||
x: params.get('x'),
|
||||
y: params.get('y')
|
||||
};
|
||||
// Try parsing parameters to integers
|
||||
if (raws.x != null) coords.x = parseInt(raws.x);
|
||||
if (raws.y != null) coords.y = parseInt(raws.y);
|
||||
if (coords.x === -1 || coords.y === -1) return json(data);
|
||||
// Return if coordinates are out of bounds
|
||||
if (coords.y < 0 || coords.y >= session.matrix.length) return json(data);
|
||||
if (coords.x < 0 || coords.x >= session.matrix[coords.y].length) return json(data);
|
||||
|
||||
// Successful because valid interaction
|
||||
data.success = true;
|
||||
|
||||
// Check if cell is a mine
|
||||
const state: boolean = session.matrix[coords.y][coords.x];
|
||||
let current = { x: coords.x, y: coords.y, mine: state, neighbors: 0 } as Cell;
|
||||
if (state) {
|
||||
// Regenerate matrix
|
||||
const h = session.matrix.length;
|
||||
const w = session.matrix[0].length;
|
||||
session.matrix = generateMatrix(w, h);
|
||||
saveSession(session);
|
||||
// Return message and only this cell
|
||||
data.message = 'Hit a mine!';
|
||||
data.cells = [current];
|
||||
return json(data);
|
||||
}
|
||||
|
||||
// Count amount of mines in adjacent cells
|
||||
const count: number = countAdjacent(current, session.matrix);
|
||||
if (count > 0) {
|
||||
// Return messaage and only this cell
|
||||
data.message = 'Calculated neighbor count.';
|
||||
current.neighbors = count;
|
||||
data.cells = [current];
|
||||
return json(data);
|
||||
}
|
||||
|
||||
// Calculate nearby empty cells until reaching the ones next to mines (flood fill/clear)
|
||||
data.message = 'Revealed all adjacent empty cells.';
|
||||
const openSet: Cell[] = [current];
|
||||
data.cells = [];
|
||||
while (openSet.length > 0) {
|
||||
// Choose (and remove) cell from openSet at random
|
||||
const index = Math.floor(Math.random() * openSet.length);
|
||||
current = openSet.splice(index, 1)[0];
|
||||
// Push cell onto closedSet
|
||||
data.cells.push(current);
|
||||
|
||||
// Go through adjacent cells and reveal them
|
||||
for (let dy = -1; dy < 2; dy++)
|
||||
for (let dx = -1; dx < 2; dx++) {
|
||||
// Ignore self
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
const nx = current.x + dx,
|
||||
ny = current.y + dy;
|
||||
|
||||
// Ignore out of bounds
|
||||
if (ny < 0 || ny >= session.matrix.length) continue;
|
||||
if (nx < 0 || nx >= session.matrix[ny].length) continue;
|
||||
const next: Cell = { x: nx, y: ny, mine: session.matrix[ny][nx], neighbors: 0 } as Cell;
|
||||
|
||||
// Ignore previously calculated
|
||||
let wasChecked = false;
|
||||
// Ignore openSet
|
||||
for (const cell of openSet)
|
||||
if (cell.x === next.x && cell.y === next.y) {
|
||||
wasChecked = true;
|
||||
break;
|
||||
}
|
||||
// Ignore closedSet
|
||||
for (const cell of data.cells)
|
||||
if (cell.x === next.x && cell.y === next.y) {
|
||||
wasChecked = true;
|
||||
break;
|
||||
}
|
||||
if (wasChecked) continue;
|
||||
|
||||
// Count amount of mines of adjaacent cell
|
||||
const nCount: number = countAdjacent(next, session.matrix);
|
||||
if (nCount > 0) {
|
||||
// Add to closedSet if next to a mine
|
||||
next.neighbors = nCount;
|
||||
data.cells.push(next);
|
||||
continue;
|
||||
}
|
||||
// Otherwise add to openSet
|
||||
openSet.push(next);
|
||||
}
|
||||
}
|
||||
|
||||
return json(data);
|
||||
}) satisfies RequestHandler;
|
13
src/routes/+layout.svelte
Normal file
13
src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
import '../app.css';
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="EMSG-background"
|
||||
class="absolute items-center justify-center w-screen h-screen bg-gray-500 opacity-50 z-50 hidden"
|
||||
/>
|
||||
<div id="EMSG-container" class="absolute items-center justify-center w-screen h-screen z-50 hidden cursor-pointer">
|
||||
<div id="EMSG-text" class="w-80 h-60 flex items-center justify-center rounded-2xl" />
|
||||
</div>
|
||||
|
||||
<slot />
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
18
svelte.config.js
Normal file
18
svelte.config.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.html', './src/**/*.js', './src/**/*.svelte', './src/**/*.ts'],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: []
|
||||
};
|
6
tests/test.ts
Normal file
6
tests/test.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('index page has expected h1', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
|
||||
});
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
9
vite.config.ts
Normal file
9
vite.config.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user