From 89c3cd91c201fafdb7994c551cd12fd92e9aa683 Mon Sep 17 00:00:00 2001 From: Tobias Christian Nauen Date: Thu, 5 Mar 2026 17:36:08 +0100 Subject: [PATCH] first ai implementation --- .gitignore | 4 + README.md | 33 ++ data.ts | 66 +++ index.html | 13 + package-lock.json | 1002 +++++++++++++++++++++++++++++++++++++++++++ package.json | 15 + public/Pokeball.png | Bin 0 -> 9768 bytes src/lib/config.ts | 5 + src/lib/game.ts | 187 ++++++++ src/lib/sampler.ts | 107 +++++ src/lib/types.ts | 84 ++++ src/main.ts | 156 +++++++ src/style.css | 349 +++++++++++++++ tsconfig.json | 19 + 14 files changed, 2040 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 data.ts create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/Pokeball.png create mode 100644 src/lib/config.ts create mode 100644 src/lib/game.ts create mode 100644 src/lib/sampler.ts create mode 100644 src/lib/types.ts create mode 100644 src/main.ts create mode 100644 src/style.css create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9451024 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.DS_Store +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7969e9 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Pokemon Type Quiz + +A fast, keyboard-driven quiz to master Pokemon type effectiveness. + +## Quick Start + +```bash +npm install +npm run dev +``` + +Open http://localhost:5173 + +## How to Play + +- **Select answer**: Press `1`-`6` (number of options shown on buttons) +- **Next question**: `Enter` or `Space` +- **Toggle dual types**: Check "Allow Dual Types" for harder questions (6 options) + +## Features + +- Weighted sampling: wrong answers appear more often, right answers less often +- Tracks ALL history in localStorage for persistent progress +- Shows detailed explanation on wrong guesses +- Monotype (default) and dual-type modes + +## Build + +```bash +npm run build +``` + +Output in `dist/` diff --git a/data.ts b/data.ts new file mode 100644 index 0000000..450efc9 --- /dev/null +++ b/data.ts @@ -0,0 +1,66 @@ +export class Data { + static types = [ + { name: "Normal", backgroundColor: "#a4acaf", color: "#212124" }, + { name: "Fire", backgroundColor: "#fd7d24", color: "#ffffff" }, + { name: "Water", backgroundColor: "#4792C4", color: "#ffffff" }, + { name: "Electric", backgroundColor: "#eed535", color: "#212121" }, + { name: "Grass", backgroundColor: "#9bcc50", color: "#212121" }, + { name: "Ice", backgroundColor: "#90d5d5", color: "#212121" }, + { name: "Fighting", backgroundColor: "#d56723", color: "#fff" }, + { name: "Poison", backgroundColor: "#b97fc9", color: "#ffffff" }, + { name: "Ground", backgroundColor: "#debb5c", color: "#212121" }, + { name: "Flying", backgroundColor: "#3dc7ef", color: "#212121" }, + { name: "Psychic", backgroundColor: "#f366b9", color: "#ffffff" }, + { name: "Bug", backgroundColor: "#7e9f56", color: "#ffffff" }, + { name: "Rock", backgroundColor: "#a38c21", color: "#ffffff" }, + { name: "Ghost", backgroundColor: "#7b62a3", color: "#ffffff" }, + { name: "Dragon", backgroundColor: "#53a4cf", color: "#ffffff" }, + { name: "Dark", backgroundColor: "#707070", color: "#ffffff" }, + { name: "Steel", backgroundColor: "#9eb7b8", color: "#212121" }, + { name: "Fairy", backgroundColor: "#fdb9e9", color: "#212121" } + ]; + + static effectiveness = [ + + // See http://pokemondb.net/type + + // NOR FIR WAT ELE GRA ICE FIG POI GRO FLY PSY BUG ROC GHO DRA DAR STE FAI + + 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 0.50, 0.00, 1.00, 1.00, 0.50, 1.00, // NOR + + 1.00, 0.50, 0.50, 1.00, 2.00, 2.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 0.50, 1.00, 0.50, 1.00, 2.00, 1.00, // FIR + + 1.00, 2.00, 0.50, 1.00, 0.50, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, 1.00, 1.00, // WAT + + 1.00, 1.00, 2.00, 0.50, 0.50, 1.00, 1.00, 1.00, 0.00, 2.00, 1.00, 1.00, 1.00, 1.00, 0.50, 1.00, 1.00, 1.00, // ELE + + 1.00, 0.50, 2.00, 1.00, 0.50, 1.00, 1.00, 0.50, 2.00, 0.50, 1.00, 0.50, 2.00, 1.00, 0.50, 1.00, 0.50, 1.00, // GRA + + 1.00, 0.50, 0.50, 1.00, 2.00, 0.50, 1.00, 1.00, 2.00, 2.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, // ICE + + 2.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, 0.50, 0.50, 0.50, 2.00, 0.00, 1.00, 2.00, 2.00, 0.50, // FIG + + 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 0.50, 0.50, 1.00, 1.00, 1.00, 0.50, 0.50, 1.00, 1.00, 0.00, 2.00, // POI + + 1.00, 2.00, 1.00, 2.00, 0.50, 1.00, 1.00, 2.00, 1.00, 0.00, 1.00, 0.50, 2.00, 1.00, 1.00, 1.00, 2.00, 1.00, // GRO + + 1.00, 1.00, 1.00, 0.50, 2.00, 1.00, 2.00, 1.00, 1.00, 1.00, 1.00, 2.00, 0.50, 1.00, 1.00, 1.00, 0.50, 1.00, // FLY + + 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 2.00, 1.00, 1.00, 0.50, 1.00, 1.00, 1.00, 1.00, 0.00, 0.50, 1.00, // PSY + + 1.00, 0.50, 1.00, 1.00, 2.00, 1.00, 0.50, 0.50, 1.00, 0.50, 2.00, 1.00, 1.00, 0.50, 1.00, 2.00, 0.50, 0.50, // BUG + + 1.00, 2.00, 1.00, 1.00, 1.00, 2.00, 0.50, 1.00, 0.50, 2.00, 1.00, 2.00, 1.00, 1.00, 1.00, 1.00, 0.50, 1.00, // ROC + + 0.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, 1.00, // GHO + + 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 0.50, 0.00, // DRA + + 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 0.50, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, 0.50, // DAR + + 1.00, 0.50, 0.50, 0.50, 1.00, 2.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 1.00, 0.50, 2.00, // STE + + 1.00, 0.50, 1.00, 1.00, 1.00, 1.00, 2.00, 0.50, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 2.00, 0.50, 1.00 // FAI + + ]; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..3c315e7 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Pokemon Type Quiz + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3dc0f6f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1002 @@ +{ + "name": "pokemon-type-quiz", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pokemon-type-quiz", + "version": "1.0.0", + "devDependencies": { + "typescript": "^5.3.3", + "vite": "^5.0.12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4d38887 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "pokemon-type-quiz", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.3.3", + "vite": "^5.0.12" + } +} diff --git a/public/Pokeball.png b/public/Pokeball.png new file mode 100644 index 0000000000000000000000000000000000000000..6dab12b81f68b924199ec3d9e176dc72ef2a7434 GIT binary patch literal 9768 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 literal 0 HcmV?d00001 diff --git a/src/lib/config.ts b/src/lib/config.ts new file mode 100644 index 0000000..1857359 --- /dev/null +++ b/src/lib/config.ts @@ -0,0 +1,5 @@ +export const config = { + wrongWeightNumerator: 1, + wrongWeightDenominator: 1, + trivialMultiplier: 0.5, +}; diff --git a/src/lib/game.ts b/src/lib/game.ts new file mode 100644 index 0000000..f4d2989 --- /dev/null +++ b/src/lib/game.ts @@ -0,0 +1,187 @@ +import { getTypeInfo, getEffectiveness } from './types'; +import { Question, Sampler } from './sampler'; + +const STORAGE_KEY = 'pokemon-type-quiz-data'; + +export interface GameState { + currentQuestion: Question | null; + selectedAnswer: number | null; + selectedIndex: number; + showResult: boolean; + isCorrect: boolean; + streak: number; + settings: { + allowDualTypes: boolean; + }; +} + +function effectivenessToLabel(value: number): { label: string; short: string } { + if (value === 0) return { label: 'No Effect (0×)', short: '0×' }; + if (value === 0.25) return { label: 'Not Very Effective (¼×)', short: '¼×' }; + if (value === 0.5) return { label: 'Not Very Effective (½×)', short: '½×' }; + if (value === 1) return { label: 'Neutral (1×)', short: '1×' }; + if (value === 2) return { label: 'Super Effective (2×)', short: '2×' }; + if (value === 4) return { label: 'Super Effective (4×)', short: '4×' }; + return { label: `${value}×`, short: `${value}×` }; +} + +export class Game { + private sampler: Sampler; + private state: GameState; + private onUpdate: () => void; + + constructor(onUpdate: () => void) { + this.onUpdate = onUpdate; + this.sampler = new Sampler(); + this.state = { + currentQuestion: null, + selectedAnswer: null, + selectedIndex: -1, + showResult: false, + isCorrect: false, + streak: 0, + settings: { + allowDualTypes: false, + }, + }; + this.load(); + } + + private load(): void { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + const data = JSON.parse(saved); + this.state.streak = data.streak || 0; + this.state.settings.allowDualTypes = data.settings?.allowDualTypes || false; + this.sampler.setAllowDualTypes(this.state.settings.allowDualTypes); + if (data.history) { + this.sampler.loadHistory(data.history); + } + } + } catch { + // Ignore parse errors + } + } + + private save(): void { + try { + const data = { + streak: this.state.streak, + settings: this.state.settings, + history: this.sampler.getHistory(), + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + } catch { + // Ignore storage errors + } + } + + getState(): GameState { + return this.state; + } + + getOptions(): { options: { value: number; label: string; short: string }[]; startKey: number } { + if (!this.state.currentQuestion) return { options: [], startKey: 1 }; + + const options = new Set(); + options.add(this.state.currentQuestion.correctAnswer); + + const allValues = [0, 0.25, 0.5, 1, 2, 4]; + for (const v of allValues) { + if (v !== this.state.currentQuestion.correctAnswer) { + options.add(v); + } + } + + const isDual = this.state.currentQuestion.defenderTypes.length > 1; + const monoValues = [2, 1, 0.5, 0]; + const dualValues = [4, 2, 1, 0.5, 0.25, 0]; + const filtered = isDual + ? dualValues.filter(v => options.has(v)) + : monoValues.filter(v => options.has(v)); + + const optionsResult = filtered.map(v => ({ value: v, ...effectivenessToLabel(v) })); + + return { options: optionsResult, startKey: isDual ? 1 : 2 }; + } + + setAllowDualTypes(allow: boolean): void { + this.state.settings.allowDualTypes = allow; + this.sampler.setAllowDualTypes(allow); + this.nextQuestion(); + this.save(); + } + + nextQuestion(): void { + this.state.currentQuestion = this.sampler.generateQuestion(); + this.state.selectedAnswer = null; + this.state.selectedIndex = -1; + this.state.showResult = false; + this.onUpdate(); + } + + selectAnswer(index: number): void { + if (this.state.showResult) return; + + const { options } = this.getOptions(); + if (index < 0 || index >= options.length) return; + + this.state.selectedIndex = index; + this.state.selectedAnswer = options[index].value; + this.state.showResult = true; + this.state.isCorrect = this.state.selectedAnswer === this.state.currentQuestion?.correctAnswer; + + if (this.state.isCorrect) { + this.state.streak++; + } else { + this.state.streak = 0; + } + + this.sampler.recordGuess( + this.state.currentQuestion!.attackType, + this.state.currentQuestion!.defenderTypes, + this.state.isCorrect + ); + + this.save(); + this.onUpdate(); + } + + getStats(): { total: number; correct: number; accuracy: number } { + const history = this.sampler.getHistory(); + const total = history.length; + const correct = history.filter(h => h.correct).length; + return { total, correct, accuracy: total > 0 ? (correct / total) * 100 : 0 }; + } + + getExplanation(): string { + if (!this.state.currentQuestion) return ''; + + const { attackType, defenderTypes } = this.state.currentQuestion; + const attackInfo = getTypeInfo(attackType); + + let explanation = `${attackInfo.name} against `; + + if (defenderTypes.length === 1) { + const defInfo = getTypeInfo(defenderTypes[0]); + explanation += `${defInfo.name}`; + } else { + const defInfos = defenderTypes.map(t => getTypeInfo(t)); + explanation += `${defInfos[0].name} / ${defInfos[1].name}`; + } + + const effective = getEffectiveness(attackType, defenderTypes); + const effectLabel = effectivenessToLabel(effective); + + explanation += `

Effectiveness: ${effectLabel.label}`; + + if (defenderTypes.length > 1) { + const first = getEffectiveness(attackType, [defenderTypes[0]]); + const second = getEffectiveness(attackType, [defenderTypes[1]]); + explanation += `
(${effectivenessToLabel(first).short} × ${effectivenessToLabel(second).short} = ${effectLabel.short})`; + } + + return explanation; + } +} diff --git a/src/lib/sampler.ts b/src/lib/sampler.ts new file mode 100644 index 0000000..e954e9a --- /dev/null +++ b/src/lib/sampler.ts @@ -0,0 +1,107 @@ +import { TypeName, TYPES, getEffectiveness, TYPE_COUNT } from './types'; +import { config } from './config'; + +export interface Question { + attackType: TypeName; + defenderTypes: TypeName[]; + correctAnswer: number; +} + +export interface GuessRecord { + key: string; + correct: boolean; + timestamp: number; +} + +function makeKey(attackType: TypeName, defenderTypes: TypeName[]): string { + return `${attackType}:${defenderTypes.sort().join('/')}`; +} + +export class Sampler { + private history: GuessRecord[] = []; + private allowDualTypes: boolean = false; + + constructor(allowDualTypes: boolean = false) { + this.allowDualTypes = allowDualTypes; + } + + setAllowDualTypes(allow: boolean): void { + this.allowDualTypes = allow; + } + + recordGuess(attackType: TypeName, defenderTypes: TypeName[], correct: boolean): void { + const key = makeKey(attackType, defenderTypes); + this.history.push({ key, correct, timestamp: Date.now() }); + } + + getHistory(): GuessRecord[] { + return this.history; + } + + loadHistory(history: GuessRecord[]): void { + this.history = history; + } + + private calculateWeight(attackType: TypeName, defenderTypes: TypeName[]): number { + const key = makeKey(attackType, defenderTypes); + const records = this.history.filter(r => r.key === key); + + const wrong = records.filter(r => !r.correct).length; + const right = records.filter(r => r.correct).length; + + let weight = (wrong + config.wrongWeightNumerator) / (right + config.wrongWeightDenominator); + + const isTrivial = defenderTypes.every(defType => + getEffectiveness(attackType, [defType]) === 1 + ); + if (isTrivial) { + weight *= config.trivialMultiplier; + } + + return weight; + } + + generateQuestion(): Question { + const candidates: { attackType: TypeName; defenderTypes: TypeName[]; weight: number }[] = []; + + for (let i = 0; i < TYPE_COUNT; i++) { + const attackType = TYPES[i].name; + + // Single type defenders - all types + for (let j = 0; j < TYPE_COUNT; j++) { + const defenderTypes = [TYPES[j].name]; + candidates.push({ attackType, defenderTypes, weight: this.calculateWeight(attackType, defenderTypes) }); + } + + if (this.allowDualTypes) { + // Dual type defenders + for (let j = 0; j < TYPE_COUNT; j++) { + for (let k = j + 1; k < TYPE_COUNT; k++) { + const defenderTypes = [TYPES[j].name, TYPES[k].name]; + candidates.push({ attackType, defenderTypes, weight: this.calculateWeight(attackType, defenderTypes) }); + } + } + } + } + + const totalWeight = candidates.reduce((sum, c) => sum + c.weight, 0); + let random = Math.random() * totalWeight; + + for (const candidate of candidates) { + random -= candidate.weight; + if (random <= 0) { + return { + attackType: candidate.attackType, + defenderTypes: candidate.defenderTypes, + correctAnswer: getEffectiveness(candidate.attackType, candidate.defenderTypes), + }; + } + } + + return candidates[0] ? { + attackType: candidates[0].attackType, + defenderTypes: candidates[0].defenderTypes, + correctAnswer: getEffectiveness(candidates[0].attackType, candidates[0].defenderTypes), + } : this.generateQuestion(); + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..c5655b3 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,84 @@ +export type TypeName = + | 'Normal' | 'Fire' | 'Water' | 'Electric' | 'Grass' | 'Ice' + | 'Fighting' | 'Poison' | 'Ground' | 'Flying' | 'Psychic' | 'Bug' + | 'Rock' | 'Ghost' | 'Dragon' | 'Dark' | 'Steel' | 'Fairy'; + +export interface TypeInfo { + name: TypeName; + backgroundColor: string; + color: string; +} + +export const TYPES: TypeInfo[] = [ + { name: 'Normal', backgroundColor: '#a4acaf', color: '#212124' }, + { name: 'Fire', backgroundColor: '#fd7d24', color: '#ffffff' }, + { name: 'Water', backgroundColor: '#4792C4', color: '#ffffff' }, + { name: 'Electric', backgroundColor: '#eed535', color: '#212121' }, + { name: 'Grass', backgroundColor: '#9bcc50', color: '#212121' }, + { name: 'Ice', backgroundColor: '#90d5d5', color: '#212121' }, + { name: 'Fighting', backgroundColor: '#d56723', color: '#ffffff' }, + { name: 'Poison', backgroundColor: '#b97fc9', color: '#ffffff' }, + { name: 'Ground', backgroundColor: '#debb5c', color: '#212121' }, + { name: 'Flying', backgroundColor: '#3dc7ef', color: '#212121' }, + { name: 'Psychic', backgroundColor: '#f366b9', color: '#ffffff' }, + { name: 'Bug', backgroundColor: '#7e9f56', color: '#ffffff' }, + { name: 'Rock', backgroundColor: '#a38c21', color: '#ffffff' }, + { name: 'Ghost', backgroundColor: '#7b62a3', color: '#ffffff' }, + { name: 'Dragon', backgroundColor: '#53a4cf', color: '#ffffff' }, + { name: 'Dark', backgroundColor: '#707070', color: '#ffffff' }, + { name: 'Steel', backgroundColor: '#9eb7b8', color: '#212121' }, + { name: 'Fairy', backgroundColor: '#fdb9e9', color: '#212121' }, +]; + +const TYPE_NAMES = TYPES.map(t => t.name); + +const EFFECTIVENESS_FLAT: number[] = [ + // NOR FIR WAT ELE GRA ICE FIG POI GRO FLY PSY BUG ROC GHO DRA DAR STE FAI + 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 0.50, 0.00, 1.00, 1.00, 0.50, 1.00, // NOR + 1.00, 0.50, 0.50, 1.00, 2.00, 2.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 0.50, 1.00, 0.50, 1.00, 2.00, 1.00, // FIR + 1.00, 2.00, 0.50, 1.00, 0.50, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, 1.00, 1.00, // WAT + 1.00, 1.00, 2.00, 0.50, 0.50, 1.00, 1.00, 1.00, 0.00, 2.00, 1.00, 1.00, 1.00, 1.00, 0.50, 1.00, 1.00, 1.00, // ELE + 1.00, 0.50, 2.00, 1.00, 0.50, 1.00, 1.00, 0.50, 2.00, 0.50, 1.00, 0.50, 2.00, 1.00, 0.50, 1.00, 0.50, 1.00, // GRA + 1.00, 0.50, 0.50, 1.00, 2.00, 0.50, 1.00, 1.00, 2.00, 2.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, // ICE + 2.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, 0.50, 0.50, 0.50, 2.00, 0.00, 1.00, 2.00, 2.00, 0.50, // FIG + 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 0.50, 0.50, 1.00, 1.00, 1.00, 0.50, 0.50, 1.00, 1.00, 0.00, 2.00, // POI + 1.00, 2.00, 1.00, 2.00, 0.50, 1.00, 1.00, 2.00, 1.00, 0.00, 1.00, 0.50, 2.00, 1.00, 1.00, 1.00, 2.00, 1.00, // GRO + 1.00, 1.00, 1.00, 0.50, 2.00, 1.00, 2.00, 1.00, 1.00, 1.00, 1.00, 2.00, 0.50, 1.00, 1.00, 1.00, 0.50, 1.00, // FLY + 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 2.00, 1.00, 1.00, 0.50, 1.00, 1.00, 1.00, 1.00, 0.00, 0.50, 1.00, // PSY + 1.00, 0.50, 1.00, 1.00, 2.00, 1.00, 0.50, 0.50, 1.00, 0.50, 2.00, 1.00, 1.00, 0.50, 1.00, 2.00, 0.50, 0.50, // BUG + 1.00, 2.00, 1.00, 1.00, 1.00, 2.00, 0.50, 1.00, 0.50, 2.00, 1.00, 2.00, 1.00, 1.00, 1.00, 1.00, 0.50, 1.00, // ROC + 0.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, 1.00, // GHO + 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 0.50, 0.00, // DRA + 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 0.50, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 2.00, 1.00, 0.50, 1.00, 0.50, // DAR + 1.00, 0.50, 0.50, 0.50, 1.00, 2.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 1.00, 1.00, 1.00, 0.50, 2.00, // STE + 1.00, 0.50, 1.00, 1.00, 1.00, 1.00, 2.00, 0.50, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 2.00, 2.00, 0.50, 1.00 // FAI +]; + +const TYPE_INDEX: Record = Object.fromEntries( + TYPE_NAMES.map((name, i) => [name, i]) +) as Record; + +export const EFFECTIVENESS_MATRIX: number[][] = []; +for (let i = 0; i < TYPE_NAMES.length; i++) { + EFFECTIVENESS_MATRIX[i] = EFFECTIVENESS_FLAT.slice(i * 18, (i + 1) * 18); +} + +export function getEffectiveness(attackType: TypeName, defenderTypes: TypeName[]): number { + let result = 1; + for (const defType of defenderTypes) { + const attackIdx = TYPE_INDEX[attackType]; + const defIdx = TYPE_INDEX[defType]; + result *= EFFECTIVENESS_MATRIX[attackIdx][defIdx]; + } + return result; +} + +export function getTypeInfo(name: TypeName): TypeInfo { + return TYPES[findTypeIndex(name)]; +} + +export function findTypeIndex(name: TypeName): number { + return TYPE_INDEX[name]; +} + +export const TYPE_COUNT = TYPES.length; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..8937eae --- /dev/null +++ b/src/main.ts @@ -0,0 +1,156 @@ +import { Game } from './lib/game'; +import { getTypeInfo, TypeName } from './lib/types'; +import './style.css'; + +const app = document.getElementById('app')!; + +const game = new Game(render); +game.nextQuestion(); + +function renderTypeBadge(name: TypeName, size: 'small' | 'large' = 'small'): string { + const info = getTypeInfo(name); + const sizeClass = size === 'large' ? 'type-badge-large' : ''; + return `${info.name}`; +} + +function render(): void { + const state = game.getState(); + const { options, startKey } = game.getOptions(); + const stats = game.getStats(); + + if (!state.currentQuestion) { + app.innerHTML = '
Loading...
'; + return; + } + + const q = state.currentQuestion; + const defenderHtml = q.defenderTypes.length === 1 + ? renderTypeBadge(q.defenderTypes[0], 'large') + : `${renderTypeBadge(q.defenderTypes[0], 'large')} / ${renderTypeBadge(q.defenderTypes[1], 'large')}`; + + const optionsHtml = options.map((opt, i) => { + let cls = 'option-btn'; + if (state.showResult) { + if (opt.value === q.correctAnswer) { + cls += ' correct'; + } else if (i === state.selectedIndex && !state.isCorrect) { + cls += ' wrong'; + } + } else if (i === state.selectedIndex) { + cls += ' selected'; + } + + const shortcut = `${startKey + i}`; + const disabled = state.showResult ? 'disabled' : ''; + + return ` + + `; + }).join(''); + + const resultHtml = state.showResult ? ` +
+ ${state.isCorrect + ? '
✓ Correct!
' + : `
✗ Wrong
+
${game.getExplanation()}
` + } + +
+ ` : ''; + + app.innerHTML = ` +
+
+

Pokemon Type Quiz

+
+ Streak: ${state.streak} + Accuracy: ${stats.accuracy.toFixed(1)}% + Total: ${stats.total} +
+
+ +
+ +
+ +
+
Attacking Type
+
${renderTypeBadge(q.attackType, 'large')}
+ +
against
+ +
Defending Type${q.defenderTypes.length > 1 ? 's' : ''}
+
${defenderHtml}
+
+ +
+ ${optionsHtml} +
+ + ${resultHtml} + + +
+ `; + + attachEventListeners(); +} + +function attachEventListeners(): void { + document.querySelectorAll('.option-btn').forEach(btn => { + btn.addEventListener('click', () => { + const index = parseInt(btn.getAttribute('data-index')!); + game.selectAnswer(index); + }); + }); + + const dualToggle = document.getElementById('dualToggle') as HTMLInputElement; + if (dualToggle) { + dualToggle.addEventListener('change', () => { + game.setAllowDualTypes(dualToggle.checked); + }); + } + + const nextBtn = document.querySelector('.next-btn'); + if (nextBtn) { + nextBtn.addEventListener('click', () => { + game.nextQuestion(); + }); + } +} + +document.addEventListener('keydown', (e) => { + const state = game.getState(); + const { options, startKey } = game.getOptions(); + + if (state.showResult) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + game.nextQuestion(); + } + return; + } + + const num = parseInt(e.key); + if (num >= startKey && num < startKey + options.length) { + e.preventDefault(); + game.selectAnswer(num - startKey); + } + + if (e.key === 'Enter') { + if (state.selectedAnswer !== null) { + e.preventDefault(); + } + } +}); + +render(); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..1298f0c --- /dev/null +++ b/src/style.css @@ -0,0 +1,349 @@ +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Outfit:wght@400;600;700&display=swap'); + +:root { + --bg-dark: #1a1a2e; + --bg-card: #16213e; + --bg-accent: #0f3460; + --text-primary: #eaeaea; + --text-secondary: #a0a0a0; + --accent: #e94560; + --success: #4ade80; + --error: #f87171; + --border-radius: 12px; + --transition: 0.2s ease; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Outfit', sans-serif; + background: var(--bg-dark); + background-image: + radial-gradient(ellipse at top, #1a1a2e 0%, #0f0f1a 100%), + repeating-linear-gradient(0deg, transparent, transparent 50px, rgba(233, 69, 96, 0.03) 50px, rgba(233, 69, 96, 0.03) 51px), + repeating-linear-gradient(90deg, transparent, transparent 50px, rgba(233, 69, 96, 0.03) 50px, rgba(233, 69, 96, 0.03) 51px); + color: var(--text-primary); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 20px; +} + +#app { + width: 100%; + max-width: 560px; +} + +.game-container { + display: flex; + flex-direction: column; + gap: 24px; +} + +.header { + text-align: center; +} + +.header h1 { + font-size: 2rem; + font-weight: 700; + background: linear-gradient(135deg, var(--accent), #ff6b6b); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 12px; + letter-spacing: -0.5px; +} + +.stats { + display: flex; + justify-content: center; + gap: 20px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.stat { + padding: 4px 12px; + background: var(--bg-card); + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.settings { + display: flex; + justify-content: center; +} + +.toggle { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + padding: 8px 16px; + background: var(--bg-card); + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.05); + transition: var(--transition); +} + +.toggle:hover { + border-color: var(--accent); +} + +.toggle input { + width: 18px; + height: 18px; + accent-color: var(--accent); +} + +.toggle-label { + font-size: 0.9rem; + color: var(--text-secondary); +} + +.question { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 32px 24px; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.question-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 1.5px; + color: var(--text-secondary); + margin-bottom: 12px; +} + +.attack-type { + margin-bottom: 16px; +} + +.type-badge { + display: inline-block; + padding: 8px 20px; + border-radius: 8px; + font-weight: 600; + font-size: 0.95rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.type-badge-large { + padding: 12px 32px; + font-size: 1.2rem; +} + +.vs { + font-size: 0.8rem; + color: var(--text-secondary); + margin: 16px 0; + font-family: 'JetBrains Mono', monospace; +} + +.defender-types { + display: flex; + justify-content: center; + gap: 8px; + flex-wrap: wrap; +} + +.options { + display: flex; + flex-direction: column; + gap: 10px; +} + +.option-btn { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + background: var(--bg-card); + border: 2px solid rgba(255, 255, 255, 0.08); + border-radius: var(--border-radius); + color: var(--text-primary); + font-family: 'Outfit', sans-serif; + font-size: 1rem; + cursor: pointer; + transition: var(--transition); + text-align: left; +} + +.option-btn:hover:not(:disabled) { + border-color: var(--accent); + transform: translateX(4px); +} + +.option-btn.selected { + border-color: var(--accent); + background: rgba(233, 69, 96, 0.1); +} + +.option-btn.correct { + border-color: var(--success); + background: rgba(74, 222, 128, 0.15); +} + +.option-btn.wrong { + border-color: var(--error); + background: rgba(248, 113, 113, 0.15); +} + +.option-btn:disabled { + cursor: default; +} + +.option-key { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: rgba(255, 255, 255, 0.1); + border-radius: 6px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + flex-shrink: 0; +} + +.option-label { + font-weight: 500; +} + +.result { + padding: 24px; + border-radius: var(--border-radius); + text-align: center; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.result.correct { + background: rgba(74, 222, 128, 0.1); + border: 1px solid rgba(74, 222, 128, 0.3); +} + +.result.wrong { + background: rgba(248, 113, 113, 0.1); + border: 1px solid rgba(248, 113, 113, 0.3); +} + +.result-title { + font-size: 1.4rem; + font-weight: 700; + margin-bottom: 8px; +} + +.result.correct .result-title { + color: var(--success); +} + +.result.wrong .result-title { + color: var(--error); +} + +.explanation { + font-size: 0.95rem; + color: var(--text-secondary); + line-height: 1.6; +} + +.explanation strong { + color: var(--text-primary); +} + +.next-btn { + margin-top: 20px; + padding: 14px 32px; + background: var(--accent); + border: none; + border-radius: var(--border-radius); + color: white; + font-family: 'Outfit', sans-serif; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: var(--transition); +} + +.next-btn:hover { + background: #ff6b6b; + transform: scale(1.02); +} + +.footer { + text-align: center; +} + +.hint { + font-size: 0.8rem; + color: var(--text-secondary); + font-family: 'JetBrains Mono', monospace; +} + +.loading { + text-align: center; + padding: 40px; + color: var(--text-secondary); +} + +@media (max-width: 480px) { + body { + padding: 12px; + } + + .header h1 { + font-size: 1.6rem; + } + + .stats { + gap: 10px; + font-size: 0.75rem; + } + + .question { + padding: 24px 16px; + } + + .type-badge-large { + padding: 10px 24px; + font-size: 1rem; + } + + .option-btn { + padding: 14px 16px; + gap: 12px; + } + + .option-key { + width: 24px; + height: 24px; + font-size: 0.75rem; + } + + .option-label { + font-size: 0.9rem; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1b0362c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +}