This commit is contained in:
Joshua Seigler 2025-04-16 14:12:23 -07:00
parent f67606b944
commit 1e71255371
9 changed files with 389 additions and 335 deletions

82
package-lock.json generated
View file

@ -10,10 +10,12 @@
"license": "MIT",
"dependencies": {
"@solid-primitives/storage": "^4.3.1",
"@solidjs/router": "^0.15.3",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.2.0",
"solid-js": "^1.9.3"
"solid-js": "^1.9.3",
"vite-plugin-inline-css-modules": "^0.0.8"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
@ -299,7 +301,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -316,7 +317,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -333,7 +333,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -350,7 +349,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -367,7 +365,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -384,7 +381,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -401,7 +397,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -418,7 +413,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -435,7 +429,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -452,7 +445,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -469,7 +461,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -486,7 +477,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -503,7 +493,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -520,7 +509,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -537,7 +525,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -554,7 +541,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -571,7 +557,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -588,7 +573,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -605,7 +589,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -622,7 +605,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -639,7 +621,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -656,7 +637,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -673,7 +653,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -690,7 +669,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -707,7 +685,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -777,7 +754,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -791,7 +767,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -805,7 +780,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -819,7 +793,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -833,7 +806,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -847,7 +819,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -861,7 +832,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -875,7 +845,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -889,7 +858,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -903,7 +871,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -917,7 +884,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -931,7 +897,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -945,7 +910,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -959,7 +923,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -973,7 +936,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -987,7 +949,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1001,7 +962,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1015,7 +975,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1029,7 +988,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1043,7 +1001,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1080,6 +1037,15 @@
"solid-js": "^1.6.12"
}
},
"node_modules/@solidjs/router": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.3.tgz",
"integrity": "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==",
"license": "MIT",
"peerDependencies": {
"solid-js": "^1.8.6"
}
},
"node_modules/@tauri-apps/api": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz",
@ -1374,7 +1340,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/babel-plugin-jsx-dom-expressions": {
@ -1530,7 +1495,6 @@
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@ -1581,7 +1545,6 @@
"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,
@ -1702,7 +1665,6 @@
"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",
@ -1741,14 +1703,12 @@
"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.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -1777,7 +1737,6 @@
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.38.0.tgz",
"integrity": "sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.7"
@ -1874,7 +1833,6 @@
"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"
@ -1933,10 +1891,9 @@
"license": "ISC"
},
"node_modules/vite": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
"dev": true,
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
@ -2004,6 +1961,15 @@
}
}
},
"node_modules/vite-plugin-inline-css-modules": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/vite-plugin-inline-css-modules/-/vite-plugin-inline-css-modules-0.0.8.tgz",
"integrity": "sha512-RXpGZJ0YW69HjkOBw/1xVeVk677wZ/OPmAbvln2eFG+9dFfq9AQapCb/iZZey/oK8psoHiwb5x2HXv5FM2UPcw==",
"license": "MIT",
"peerDependencies": {
"vite": ">2.0.0-0"
}
},
"node_modules/vite-plugin-solid": {
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.6.tgz",

View file

@ -13,10 +13,12 @@
"license": "MIT",
"dependencies": {
"@solid-primitives/storage": "^4.3.1",
"@solidjs/router": "^0.15.3",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.2.0",
"solid-js": "^1.9.3"
"solid-js": "^1.9.3",
"vite-plugin-inline-css-modules": "^0.0.8"
},
"devDependencies": {
"@tauri-apps/cli": "^2",

View file

@ -1,141 +1,18 @@
import { For } from "solid-js";
import "./App.css";
import useAppModel from "./useAppModel";
import FitText from "./FitText";
import { type JSX } from "solid-js"
// TODO
// add routing
// make overview page with calendar list of puzzles
// show solved / aced / busted
// make detail page with puzzle id in path
// add nav links
function App() {
const {
connections,
store,
setStore,
handleGuess,
handlePinUnpin,
handleSelectGame,
handleShuffle,
handleDeselect,
getFromPuzzle,
} = useAppModel();
return (
<main class="container">
<div class="puzzle">
<header class="puzzle-header">
<h1>Slick Connections</h1>
<div class="puzzle-header-actions">
<button
type="button"
on:click={() => {
handleSelectGame(store.puzzleId - 1);
}}
disabled={store.puzzleId === 1}
>
-
</button>
<select
name="puzzleNumber"
id="puzzleNumber"
on:input={({ target: { value } }) =>
handleSelectGame(parseInt(value, 10))
}
value={store.puzzleId}
>
<For each={connections}>
{({ id }) => <option value={id}>{id}</option>}
</For>
</select>
<button
type="button"
on:click={() => {
handleSelectGame(store.puzzleId + 1);
}}
disabled={
store.puzzleId === connections[connections.length - 1].id
}
>
+
</button>
</div>
</header>
<div>Create four groups of four words!</div>
<For each={store.solvedGroups}>
{({ group, level, members }) => (
<div class="puzzle-row">
<div class="puzzle-group" data-level={level}>
<div class="puzzle-group-name">{group}</div>
<div class="puzzle-group-members">{members.join(", ")}</div>
</div>
</div>
)}
</For>
<For each={[0, 1, 2, 3].slice(0, store.puzzle.length / 4)}>
{(row) => (
<div class="puzzle-row">
{[0, 1, 2, 3].map((col) => {
const index = 4 * row + col;
const answer = () => getFromPuzzle(index).answer;
return (
<button
classList={{
"puzzle-item": true,
"is-selected": store.selected.includes(index),
}}
style={{ "view-transition-name": `puzzle-${answer}` }}
type="button"
on:click={() => {
setStore({
selected: store.selected.includes(index)
? store.selected.filter((x) => x !== index)
: [...store.selected, index],
});
}}
>
<FitText body={answer} />
{store.pinnedCount > index && <div class="badge">🔒</div>}
</button>
);
})}
</div>
)}
</For>
<div class="puzzle-actions">
<button
type="button"
disabled={store.selected.length === 0}
on:click={handlePinUnpin}
>
{store.selected.length > 0 &&
store.selected.every((x) => x < store.pinnedCount)
? "Unpin"
: "Pin"}
</button>
<button type="button" on:click={handleShuffle}>
Shuffle
</button>
<button type="button" on:click={handleDeselect} disabled={
store.selected.length === 0
}>
Deselect all
</button>
<button
id="submitButton"
type="button"
on:click={handleGuess}
disabled={store.selected.length !== 4}
>
Submit
</button>
</div>
</div>
{ store.solvedGroups.length === 4 && <div class="celebration" /> }
</main>
);
type AppProps = {
children?: JSX.Element
}
export default App;
export default function App({ children }: AppProps) {
return (
<>
<header>
<h1>Slick Connections</h1>
</header>
<main>
{children}
</main>
</>
)
}

36
src/Dashboard.tsx Normal file
View file

@ -0,0 +1,36 @@
import { For } from "solid-js";
import useAppModel from "./useAppModel";
import { A } from "@solidjs/router";
import { css } from 'vite-plugin-inline-css-modules'
const styles = css`
calendar: {
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
},
entry: (solved: boolean) => {
return {
borderRadius: "50%",
backgroundColor: solved ? "green" : "gray",
}
},
`
export default function Dashboard() {
const { connections, store } = useAppModel();
return (
<div class={styles.calendar}>
<For each={connections}>
{(item) => {
const isSolved = store.solutions[item.id] !== undefined;
return (
<A
href={`/puzzle/${item.id}`}
{...stylex.props(styles.entry(isSolved))}
/>
);
}}
</For>
</div>
);
}

134
src/Puzzle.tsx Normal file
View file

@ -0,0 +1,134 @@
import { For } from "solid-js";
import "./App.css";
import usePuzzleModel from "./usePuzzleModel";
import FitText from "./FitText";
import { useNavigate, useParams } from "@solidjs/router";
// TODO
// add routing
// make overview page with calendar list of puzzles
// show solved / aced / busted
// make detail page with puzzle id in path
// add nav links
function Puzzle() {
const params = useParams<{ id: string }>()
const id = () => parseInt(params.id);
const {
connections,
store,
setStore,
handleGuess,
handlePinUnpin,
handleShuffle,
handleDeselect,
getFromPuzzle,
} = usePuzzleModel(id);
const navigate = useNavigate()
return (
<main class="container">
<div class="puzzle">
<header class="puzzle-header">
<h1>Slick Connections</h1>
<div class="puzzle-header-actions">
<button
type="button"
on:click={() => {
navigate(`/puzzle/${id() - 1}`);
}}
disabled={id() === 1}
>
-
</button>
<button
type="button"
on:click={() => {
navigate(`/puzzle/${id() + 1}`);
}}
disabled={id() === connections.length - 1}
>
+
</button>
</div>
</header>
<div>Create four groups of four words!</div>
<For each={store.solvedGroups}>
{({ group, level, members }) => (
<div class="puzzle-row">
<div class="puzzle-group" data-level={level}>
<div class="puzzle-group-name">{group}</div>
<div class="puzzle-group-members">{members.join(", ")}</div>
</div>
</div>
)}
</For>
<For each={[0, 1, 2, 3].slice(0, store.puzzle.length / 4)}>
{(row) => (
<div class="puzzle-row">
{[0, 1, 2, 3].map((col) => {
const index = 4 * row + col;
const answer = () => getFromPuzzle(index).answer;
return (
<button
classList={{
"puzzle-item": true,
"is-selected": store.selected.includes(index),
}}
style={{ "view-transition-name": `puzzle-${answer}` }}
type="button"
on:click={() => {
setStore({
selected: store.selected.includes(index)
? store.selected.filter((x) => x !== index)
: [...store.selected, index],
});
}}
>
<FitText body={answer} />
{store.pinnedCount > index && <div class="badge">🔒</div>}
</button>
);
})}
</div>
)}
</For>
<div class="puzzle-actions">
<button
type="button"
disabled={store.selected.length === 0}
on:click={handlePinUnpin}
>
{store.selected.length > 0 &&
store.selected.every((x) => x < store.pinnedCount)
? "Unpin"
: "Pin"}
</button>
<button type="button" on:click={handleShuffle}>
Shuffle
</button>
<button
type="button"
on:click={handleDeselect}
disabled={store.selected.length === 0}
>
Deselect all
</button>
<button
id="submitButton"
type="button"
on:click={handleGuess}
disabled={store.selected.length !== 4}
>
Submit
</button>
</div>
</div>
{store.solvedGroups.length === 4 && <div class="celebration" />}
</main>
);
}
export default Puzzle;

View file

@ -1,5 +1,22 @@
/* @refresh reload */
import { render } from "solid-js/web";
import App from "./App";
import { Route, Router } from "@solidjs/router";
import Dashboard from "./Dashboard";
import Puzzle from "./Puzzle";
render(() => <App />, document.getElementById("root") as HTMLElement);
render(
() => (
<Router root={App}>
<Route path="/" component={Dashboard} />
<Route
path="/puzzle/:id"
component={Puzzle}
matchFilters={{
id: /^\d+$/,
}}
/>
</Router>
),
document.getElementById("root") as HTMLElement
);

View file

@ -1,24 +1,15 @@
import { makePersisted } from "@solid-primitives/storage";
import connections from "./assets/connections.json";
import { shuffleArray } from "./utils";
import { createStore } from "solid-js/store";
import { tauriStorage } from "@solid-primitives/storage/tauri";
type Connection = (typeof connections)[number];
type Answer = Connection["answers"][number];
function fromIndex(index: number): [number, number] {
const col = index % 4;
const row = (index - col) / 4;
return [row, col];
type Solution = {
id: number
mistakes: number
}
type AppStore = {
puzzleId: number;
pinnedCount: number;
selected: number[];
solvedGroups: Answer[];
puzzle: number[];
solutions: Solution[]
};
export default function useAppModel() {
@ -26,138 +17,23 @@ export default function useAppModel() {
"__TAURI_INTERNALS__" in window ? tauriStorage("AppStore") : localStorage;
const [store, setStore] = makePersisted(
createStore<AppStore>({
puzzleId: 1,
pinnedCount: 0,
selected: [],
solvedGroups: [],
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)),
solutions: []
}),
{
name: "slick-connections",
storage,
serialize(data) {
return JSON.stringify({
...data,
selected: [],
});
},
}
);
const answers = (): Answer[] => {
return connections.find((x) => x.id === store.puzzleId)!.answers;
};
const handleSelectGame = (newId: number) => {
setStore({
puzzleId: newId,
selected: [],
pinnedCount: 0,
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)),
solvedGroups: [],
});
};
const getFromPuzzle = (index: number) => {
const puzzleIndex = store.puzzle[index];
const [groupIndex, memberIndex] = fromIndex(puzzleIndex);
const group = answers()[groupIndex];
return {
group: group.group,
level: group.level,
answer: answers()[groupIndex].members[memberIndex],
};
};
const handleGuess = () => {
const selected = store.puzzle.length === 4 ? [0, 1, 2, 3] : store.selected;
const selectedAnswers = selected.map((x) => getFromPuzzle(x));
const { level } = selectedAnswers[0];
const isCorrect = selectedAnswers.every((x) => x.level === level);
if (!isCorrect) {
// TODO you got it wrong
alert("wrong");
return;
}
const selectedPinnedCount = selected.reduce(
(acc, cur) => acc + (cur < store.pinnedCount ? 1 : 0),
0
);
setStore({
pinnedCount: store.pinnedCount - selectedPinnedCount,
puzzle: store.puzzle.filter((x) =>
selected.every((s) => store.puzzle[s] !== x)
),
selected: [],
});
const newSolvedGroup = answers().find((x) => x.level === level);
if (newSolvedGroup != null) {
setStore({
solvedGroups: [...store.solvedGroups, newSolvedGroup],
});
}
if (store.puzzle.length === 0) {
// completely solved!
}
};
const handleShuffle = () => {
const pinned = store.puzzle.slice(0, store.pinnedCount);
const toShuffle = store.puzzle.slice(store.pinnedCount);
setStore({
puzzle: [...pinned, ...shuffleArray(toShuffle)],
});
};
const handleDeselect = () => {
setStore({
selected: [],
});
};
const handlePinUnpin = () => {
if (store.selected.every((x) => x < store.pinnedCount)) {
// we are unpinning
const puzzleStart = Array.from({ length: store.pinnedCount }, (_, i) => i)
.filter((x) => !store.selected.includes(x))
.map((x) => store.puzzle[x]);
const puzzleMiddle = store.selected.map((x) => store.puzzle[x]);
const puzzleEnd = store.puzzle.slice(store.pinnedCount);
const newPuzzle = [...puzzleStart, ...puzzleMiddle, ...puzzleEnd];
setStore({
pinnedCount: store.pinnedCount - store.selected.length,
selected: [],
puzzle: newPuzzle,
});
return;
}
// we are pinning
const puzzleStart = store.puzzle.slice(0, store.pinnedCount);
const puzzleMid = store.selected
.filter((x) => x >= store.pinnedCount)
.map((x) => store.puzzle[x]);
const puzzleEnd = Array.from(
{ length: store.puzzle.length - store.pinnedCount },
(_, i) => i + store.pinnedCount
)
.filter((x) => !store.selected.includes(x))
.map((x) => store.puzzle[x]);
setStore({
pinnedCount: puzzleStart.length + puzzleMid.length,
selected: [],
puzzle: [...puzzleStart, ...puzzleMid, ...puzzleEnd],
});
};
function setSolution(id: number, mistakes: number) {
setStore("solutions", id, {
id,
mistakes
})
}
return {
connections,
store,
setStore,
handleGuess,
handlePinUnpin,
handleSelectGame,
handleShuffle,
handleDeselect,
getFromPuzzle,
setSolution,
};
}

145
src/usePuzzleModel.ts Normal file
View file

@ -0,0 +1,145 @@
import { Accessor, createEffect } from "solid-js";
import connections from "./assets/connections.json";
import { shuffleArray } from "./utils";
import { createStore } from "solid-js/store";
type Connection = (typeof connections)[number];
type Answer = Connection["answers"][number];
function fromIndex(index: number): [number, number] {
const col = index % 4;
const row = (index - col) / 4;
return [row, col];
}
type AppStore = {
pinnedCount: number;
selected: number[];
solvedGroups: Answer[];
puzzle: number[];
};
export default function usePuzzleModel(id: Accessor<number>) {
const [store, setStore] = createStore<AppStore>({
pinnedCount: 0,
selected: [],
solvedGroups: [],
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)),
});
createEffect(() => {
id()
setStore({
pinnedCount: 0,
selected: [],
solvedGroups: [],
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)),
});
})
const answers = (): Answer[] => {
return connections.find((x) => x.id === id())!.answers;
};
const getFromPuzzle = (index: number) => {
const puzzleIndex = store.puzzle[index];
const [groupIndex, memberIndex] = fromIndex(puzzleIndex);
const group = answers()[groupIndex];
return {
group: group.group,
level: group.level,
answer: answers()[groupIndex].members[memberIndex],
};
};
const handleGuess = () => {
const selected = store.puzzle.length === 4 ? [0, 1, 2, 3] : store.selected;
const selectedAnswers = selected.map((x) => getFromPuzzle(x));
const { level } = selectedAnswers[0];
const isCorrect = selectedAnswers.every((x) => x.level === level);
if (!isCorrect) {
// TODO you got it wrong
alert("wrong");
return;
}
const selectedPinnedCount = selected.reduce(
(acc, cur) => acc + (cur < store.pinnedCount ? 1 : 0),
0
);
setStore({
pinnedCount: store.pinnedCount - selectedPinnedCount,
puzzle: store.puzzle.filter((x) =>
selected.every((s) => store.puzzle[s] !== x)
),
selected: [],
});
const newSolvedGroup = answers().find((x) => x.level === level);
if (newSolvedGroup != null) {
setStore({
solvedGroups: [...store.solvedGroups, newSolvedGroup],
});
}
if (store.puzzle.length === 0) {
// completely solved!
}
};
const handleShuffle = () => {
const pinned = store.puzzle.slice(0, store.pinnedCount);
const toShuffle = store.puzzle.slice(store.pinnedCount);
setStore({
puzzle: [...pinned, ...shuffleArray(toShuffle)],
});
};
const handleDeselect = () => {
setStore({
selected: [],
});
};
const handlePinUnpin = () => {
if (store.selected.every((x) => x < store.pinnedCount)) {
// we are unpinning
const puzzleStart = Array.from({ length: store.pinnedCount }, (_, i) => i)
.filter((x) => !store.selected.includes(x))
.map((x) => store.puzzle[x]);
const puzzleMiddle = store.selected.map((x) => store.puzzle[x]);
const puzzleEnd = store.puzzle.slice(store.pinnedCount);
const newPuzzle = [...puzzleStart, ...puzzleMiddle, ...puzzleEnd];
setStore({
pinnedCount: store.pinnedCount - store.selected.length,
selected: [],
puzzle: newPuzzle,
});
return;
}
// we are pinning
const puzzleStart = store.puzzle.slice(0, store.pinnedCount);
const puzzleMid = store.selected
.filter((x) => x >= store.pinnedCount)
.map((x) => store.puzzle[x]);
const puzzleEnd = Array.from(
{ length: store.puzzle.length - store.pinnedCount },
(_, i) => i + store.pinnedCount
)
.filter((x) => !store.selected.includes(x))
.map((x) => store.puzzle[x]);
setStore({
pinnedCount: puzzleStart.length + puzzleMid.length,
selected: [],
puzzle: [...puzzleStart, ...puzzleMid, ...puzzleEnd],
});
};
return {
connections,
store,
setStore,
handleGuess,
handlePinUnpin,
handleShuffle,
handleDeselect,
getFromPuzzle,
};
}

View file

@ -1,12 +1,13 @@
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
import vitePluginInlineCSSModules from 'vite-plugin-inline-css-modules'
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [solid()],
plugins: [solid(), vitePluginInlineCSSModules()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//