feat: single player
This commit is contained in:
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
|
[*.{js,jsx,ts,tsx,css,html}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install # or pnpm install or yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm run dev`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `dist` folder.<br>
|
||||||
|
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Learn more about deploying your application with the [documentations](https://vite.dev/guide/static-deploy.html)
|
||||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>路墙棋</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3351
package-lock.json
generated
Normal file
3351
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "quoridor",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@solidjs/router": "^0.15.3",
|
||||||
|
"solid-js": "^1.9.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vite": "^7.1.7",
|
||||||
|
"vite-plugin-singlefile": "^2.3.0",
|
||||||
|
"vite-plugin-solid": "^2.11.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
37
src/App.tsx
Normal file
37
src/App.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createEffect } from 'solid-js';
|
||||||
|
import { Route, Router, useNavigate, useSearchParams } from '@solidjs/router'
|
||||||
|
import Home from './Home'
|
||||||
|
import Room from './Room'
|
||||||
|
import Game from './Game';
|
||||||
|
import { LocalGameStatus, gameStatusFromUrl } from './model';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Route path="/" component={() => {
|
||||||
|
const [params, setParams] = useSearchParams<{ p: string }>();
|
||||||
|
const game = params.p ? gameStatusFromUrl(params.p) : new LocalGameStatus(0);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (game.state.moves.length > 0) {
|
||||||
|
setParams({ p: game.toUrlQuery() }, { replace: true });
|
||||||
|
} else if (params.p) {
|
||||||
|
// setParams({ p: "" }) will remove `p`
|
||||||
|
const newUrl = `${location.pathname}?p=`;
|
||||||
|
useNavigate()(newUrl, { replace: true, scroll: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (params.p === "") {
|
||||||
|
game.clear(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return <>{params.p === undefined ? <Home /> : <Game offline={true} game={game}/>}</>;
|
||||||
|
}} />
|
||||||
|
<Route path="/:roomId" component={Room} />
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
127
src/Board.tsx
Normal file
127
src/Board.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { children, createEffect, createSignal, For, Show, type Accessor, type JSXElement } from "solid-js";
|
||||||
|
import type { Action, LocalGameStatus } from "./model";
|
||||||
|
|
||||||
|
function Board({ player, game, movable }: { player?: 0 | 1, game: LocalGameStatus, movable: Accessor<boolean> }) {
|
||||||
|
const [action, setAction] = createSignal<Action>("move");
|
||||||
|
const state = game.state;
|
||||||
|
const colorText = (player: 0 | 1) => player === 0 ? "红" : "蓝";
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (player && state.wallsLeft[player] <= 0) {
|
||||||
|
setAction("move")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="mt-4 space-x-4 flex items-center justify-center">
|
||||||
|
<span class="text-xl">
|
||||||
|
{state.winner === undefined ? `请${colorText(state.curPlayer)}方操作` : `${colorText(state.winner)}方胜利`}
|
||||||
|
</span>
|
||||||
|
<button onclick={() => game.undo()} type="button" class="inline-block overflow-hidden whitespace-nowrap transition-colors rounded-full box-shadow h-6 leading-6 px-2 text-xs text-black/90 bg-slate-200/80 active:bg-slate-300/70">
|
||||||
|
悔棋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<svg id="svg" viewBox="-53,-53,106,106" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
{/* board */}
|
||||||
|
<path stroke="#272E3B" stroke-width="0.25" fill="none" d="m-45,-45h90v90h-90zm10,0v90m10,0v-90m10,0v90m10,0v-90m10,0v90m10,0v-90m10,0v90m10,0v-90m10,10h-90m0,10h90m0,10h-90m0,10h90m0,10h-90m0,10h90m0,10h-90m0,10h90" />
|
||||||
|
|
||||||
|
<For each={[...Array(9).keys()]}>{lineNumber}</For>
|
||||||
|
<For each={[...Array(9).keys()]}>{columnNumber}</For>
|
||||||
|
|
||||||
|
{/* goals */}
|
||||||
|
<rect x="-45" y="-45" width="90" height="10" fill="#f53f3f" fill-opacity="0.2"></rect>
|
||||||
|
<rect x="-45" y="35" width="90" height="10" fill="#165dff" fill-opacity="0.2"></rect>
|
||||||
|
|
||||||
|
{/* pieces */}
|
||||||
|
<circle cx={calcX(state.coords[0].x)} cy={calcY(state.coords[0].y)} r="2.3" stroke-width="1.3" stroke="#f53f3f" fill="none"></circle>
|
||||||
|
<circle cx={calcX(state.coords[1].x)} cy={calcY(state.coords[1].y)} r="2.3" stroke-width="1.3" stroke="#165dff" fill="none"></circle>
|
||||||
|
|
||||||
|
{/* walls */}
|
||||||
|
<For each={game.horizontalWalls()}>
|
||||||
|
{({ x, y }) => (
|
||||||
|
<rect x={calcX(x) - 5} y={calcY(y) - 6.5} width="20" height="3" fill="black"></rect>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<For each={game.verticalWalls()}>
|
||||||
|
{({ x, y }) => (
|
||||||
|
<rect x={calcX(x) + 3.5} y={calcY(y) - 15} width="3" height="20" fill="black"></rect>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
{/* available destinations */}
|
||||||
|
<Show when={movable() && action() === "move"}>
|
||||||
|
<For each={game.availableDests()}>
|
||||||
|
{({ x, y }) => (
|
||||||
|
<g>
|
||||||
|
<circle cx={calcX(x)} cy={calcY(y)} r="2.3" stroke-width="1.3" stroke={state.curPlayer == 0 ? "#f53f3f" : "#165dff"} stroke-opacity="0.4" fill="none"></circle>
|
||||||
|
<rect onclick={() => { game.move({ x, y }) }} class="cursor-pointer" x={calcX(x) - 5} y={calcY(y) - 5} width="10" height="10" fill="black" fill-opacity="0"></rect>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* available walls */}
|
||||||
|
<Show when={movable() && action() === "horizontal"}>
|
||||||
|
<For each={game.availableHorizontalWalls()}>
|
||||||
|
{({ x, y }) => (
|
||||||
|
<rect onclick={() => { game.placeWall({ x, y }, "horizontal") }} class="cursor-pointer" x={calcX(x) + 1} y={calcY(y) - 6.5} width="8" height="3" fill="#6b7280"></rect>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
<Show when={movable() && action() === "vertical"}>
|
||||||
|
<For each={game.availableVerticalWalls()}>
|
||||||
|
{({ x, y }) => (
|
||||||
|
<rect onclick={() => { game.placeWall({ x, y }, "vertical") }} class="cursor-pointer" x={calcX(x) + 3.5} y={calcY(y) - 9} width="3" height="8" fill="#6b7280"></rect>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<Show when={movable()}>
|
||||||
|
<div class="flex flex-wrap items-center justify-center space-x-4">
|
||||||
|
<Button active={action() === "move"} onclick={() => setAction("move")}>移动棋子</Button>
|
||||||
|
<Button active={action() === "horizontal"} onclick={() => state.wallsLeft[state.curPlayer] > 0 && setAction("horizontal")}>放置水平墙</Button>
|
||||||
|
<Button active={action() === "vertical"} onclick={() => state.wallsLeft[state.curPlayer] > 0 && setAction("vertical")}>放置竖直墙</Button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineNumber = (line: number) =>
|
||||||
|
<text
|
||||||
|
alignment-baseline="central"
|
||||||
|
dominant-baseline="central"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="3"
|
||||||
|
x="-49"
|
||||||
|
y={calcY(line)}
|
||||||
|
>
|
||||||
|
{line + 1}
|
||||||
|
</text>;
|
||||||
|
const columnNumber = (column: number) =>
|
||||||
|
<text
|
||||||
|
alignment-baseline="central"
|
||||||
|
dominant-baseline="central"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="3"
|
||||||
|
x={calcX(column)}
|
||||||
|
y="49"
|
||||||
|
>
|
||||||
|
{String.fromCharCode(65 + column) /* 65 == 'A' */}
|
||||||
|
</text>;
|
||||||
|
const calcX = (index: number) => (index - 8) * 10 + 40;
|
||||||
|
const calcY = (index: number) => -calcX(index);
|
||||||
|
|
||||||
|
function Button(props: { active: boolean, children?: JSXElement, [others: string]: unknown }) {
|
||||||
|
const resolved = children(() => props.children);
|
||||||
|
return (
|
||||||
|
<button {...props} type="button" class={`inline-block overflow-hidden whitespace-nowrap transition-colors rounded-full box-shadow h-10 leading-10 px-4 text-base my-1 ${props.active ? "text-black/40 bg-gray-200 cursor-not-allowed" : "text-black/90 bg-slate-200/80 active:bg-slate-300/70"}`}>
|
||||||
|
{resolved()}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Board;
|
||||||
74
src/Game.tsx
Normal file
74
src/Game.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { LocalGameStatus } from "./model";
|
||||||
|
import Board from "./Board";
|
||||||
|
import { createEffect, createSignal, Show } from "solid-js";
|
||||||
|
|
||||||
|
function Game({ offline, player, game, roomId }: { offline?: boolean, player?: 0 | 1, game: LocalGameStatus, roomId?: string }) {
|
||||||
|
const [movable, setMovable] = createSignal(true);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (game.state.curPlayer === player || offline && game.state.winner === undefined) {
|
||||||
|
setMovable(true);
|
||||||
|
} else {
|
||||||
|
setMovable(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="select-none flex flex-grow flex-col min-h-screen">
|
||||||
|
<div class="relative">
|
||||||
|
<div style="left: 10%; position: absolute; top: 24px;">
|
||||||
|
<a class="inline-block overflow-hidden whitespace-nowrap transition-colors rounded-full box-shadow h-6 leading-6 px-2 text-xs text-black/90 bg-slate-200/80 active:bg-slate-300/70" href="/">
|
||||||
|
回到主页
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-2 flex items-center justify-center">
|
||||||
|
<span class="text-xl">路墙棋</span>
|
||||||
|
</div>
|
||||||
|
<Show when={roomId !== undefined}>
|
||||||
|
<div class="text-center text-sm mb-4">房间号: {roomId}</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="flex justify-evenly mt-4">
|
||||||
|
<div class="w-20 flex flex-col items-center">
|
||||||
|
<div class="relative box-shadow w-14 h-14 rounded-full border text-center flex items-center justify-center mx-4 my-2">
|
||||||
|
<div class="text-2xl overflow-hidden">
|
||||||
|
player1
|
||||||
|
</div>
|
||||||
|
<div class="absolute whitespace-nowrap px-1 translate-x-[-50%] rounded-full text-white text-xs bg-orange-600 top-0 left-[7px]">
|
||||||
|
房主
|
||||||
|
</div>
|
||||||
|
<div class="absolute whitespace-nowrap px-1 translate-x-[-50%] rounded-full text-white text-xs bg-orange-600 font-mono top-[39px] left-[46px]">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div class="absolute whitespace-nowrap px-1 translate-x-[-50%] rounded-full text-white text-xs bg-orange-600 top-[39px] left-[7px]">
|
||||||
|
我
|
||||||
|
</div>
|
||||||
|
<button type="button" class="block absolute w-14 h-14 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 w-6 h-6 mx-auto text-center font-bold" style="color: rgb(245, 63, 63);">○</div>
|
||||||
|
<div>剩 {game.state.wallsLeft[0]} 墙</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-20 flex flex-col items-center">
|
||||||
|
<div class="relative box-shadow w-14 h-14 rounded-full border text-center flex items-center justify-center mx-4 my-2">
|
||||||
|
<div class="text-2xl overflow-hidden">
|
||||||
|
player2
|
||||||
|
</div>
|
||||||
|
<div class="absolute whitespace-nowrap px-1 translate-x-[-50%] rounded-full text-white text-xs bg-gray-600 font-mono top-[39px] left-[46px]">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div class="absolute left-[3px] top-[42px] border border-black rounded-full w-2.5 h-2.5 bg-gray-500" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 w-6 h-6 mx-auto text-center font-bold" style="color: rgb(22, 93, 255);">○</div>
|
||||||
|
<div>剩 {game.state.wallsLeft[1]} 墙</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Board player={player} game={game} movable={movable} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default Game;
|
||||||
47
src/Home.tsx
Normal file
47
src/Home.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="flex-grow flex flex-col">
|
||||||
|
<h1 class="py-12 flex items-center justify-center">
|
||||||
|
<span class="text-3xl font-bold">路墙棋</span>
|
||||||
|
</h1>
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<div>
|
||||||
|
<a class="inline-block overflow-hidden whitespace-nowrap transition-colors rounded-full box-shadow h-10 leading-10 px-4 text-base text-black/90 bg-slate-200/80 active:bg-slate-300/70" href="/?p">
|
||||||
|
👥 本地对战
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a class="inline-block overflow-hidden whitespace-nowrap transition-colors rounded-full box-shadow h-10 leading-10 px-4 text-base text-white primary" href="/ntak">
|
||||||
|
👥 创建房间
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="w-56 mx-auto">
|
||||||
|
<input type="text" placeholder="输入房间号,进指定房间" data-ddg-inputtype="unknown" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="inline-block overflow-hidden whitespace-nowrap transition-colors rounded-full box-shadow h-10 leading-10 px-4 text-base text-black/40 bg-gray-200 cursor-not-allowed">
|
||||||
|
👥 进入房间
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-xs mx-auto px-2 space-y-2 text-center">
|
||||||
|
<div>最近进入过的房间</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-44 text-left truncate font-mono">
|
||||||
|
🏠 placeholder
|
||||||
|
</div>
|
||||||
|
<div class="w-20 truncate">
|
||||||
|
刚刚
|
||||||
|
</div>
|
||||||
|
<a class="inline-block overflow-hidden whitespace-nowrap transition-colors rounded-full box-shadow h-6 leading-6 px-2 text-xs text-black/90 bg-slate-200/80 active:bg-slate-300/70 ml-auto" href="/ofa1">
|
||||||
|
进入
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
61
src/Room.tsx
Normal file
61
src/Room.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { useParams } from "@solidjs/router";
|
||||||
|
|
||||||
|
function Room() {
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="flex-grow flex flex-col">
|
||||||
|
<div class="pt-2 flex items-center justify-center">
|
||||||
|
<span class="text-xl">路墙棋</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-center text-sm mb-4">房间号: {params.roomId}</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap justify-center -my-2">
|
||||||
|
<div class="relative box-shadow w-14 h-14 rounded-full border text-center flex items-center justify-center mx-4 my-2">
|
||||||
|
<div class="text-2xl overflow-hidden">
|
||||||
|
player1
|
||||||
|
</div>
|
||||||
|
<div class="absolute whitespace-nowrap px-1 translate-x-[-50%] rounded-full text-white text-xs bg-orange-600 top-0 left-[7px]">
|
||||||
|
房主
|
||||||
|
</div>
|
||||||
|
<div class="absolute whitespace-nowrap px-1 translate-x-[-50%] rounded-full text-white text-xs bg-orange-600 font-mono top-[39px] left-[46px]">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div class="absolute whitespace-nowrap px-1 translate-x-[-50%] rounded-full text-white text-xs bg-orange-600 top-[39px] left-[7px]">
|
||||||
|
我
|
||||||
|
</div>
|
||||||
|
<button type="button" class="block absolute w-14 h-14 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<div class="relative box-shadow w-14 h-14 rounded-full border text-center flex items-center justify-center mx-4 my-2">
|
||||||
|
<div class="text-2xl overflow-hidden">
|
||||||
|
player2
|
||||||
|
</div>
|
||||||
|
<div class="absolute whitespace-nowrap px-1 translate-x-[-50%] rounded-full text-white text-xs bg-gray-600 font-mono top-[39px] left-[46px]">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div class="absolute left-[3px] top-[42px] border border-black rounded-full w-2.5 h-2.5 bg-gray-500" />
|
||||||
|
<button type="button" class="absolute rounded-full block h-6 w-6 leading-6 -top-1 left-9 bg-red-500 text-white active:bg-red-600 box-shadow">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mt-8 mb-12">
|
||||||
|
<button type="button" class="inline-block overflow-hidden whitespace-nowrap transition-colors rounded-full box-shadow h-6 leading-6 px-2 text-xs text-black/90 bg-slate-200/80 active:bg-slate-300/70">
|
||||||
|
离开座位,观战
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="my-8">
|
||||||
|
<button type="button" class="inline-block overflow-hidden whitespace-nowrap transition-colors rounded-full box-shadow h-10 leading-10 px-4 text-base text-white primary">
|
||||||
|
开始游戏
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Room;
|
||||||
1
src/assets/solid.svg
Normal file
1
src/assets/solid.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" fill="#76b3e1"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="27.5" y1="3" x2="152" y2="63.5"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" opacity=".3" fill="url(#a)"/><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" fill="#518ac8"/><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="95.8" y1="32.6" x2="74" y2="105.2"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" opacity=".3" fill="url(#b)"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="18.4" y1="64.2" x2="144.3" y2="149.8"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><path d="M134 80a45 45 0 00-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" fill="url(#c)"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="75.2" y1="74.5" x2="24.4" y2="260.8"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient><path d="M114 115a45 45 0 00-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" fill="url(#d)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
37
src/index.css
Normal file
37
src/index.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #f0f8ff
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: #4fb9fc;
|
||||||
|
color: #fff
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary:active {
|
||||||
|
background: #3f94ca
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text]:focus {
|
||||||
|
outline-color: #3b82f6
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
color: rgba(0,0,0,.9);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 3px 7px -1px rgba(0,0,0,.2),0 2px 5px -2px rgba(0,0,0,.2);
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-shadow {
|
||||||
|
box-shadow:0 3px 7px -1px rgba(0,0,0,.2),0 2px 5px -2px rgba(0,0,0,.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.box-shadow:active,
|
||||||
|
button.box-shadow:active {
|
||||||
|
box-shadow:0 6px 12px -1px rgba(0,0,0,.3),0 3px 9px -2px rgba(0,0,0,.3)
|
||||||
|
}
|
||||||
10
src/index.tsx
Normal file
10
src/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
import { render } from 'solid-js/web'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import Game from './Game.tsx'
|
||||||
|
import { LocalGameStatus } from './model.tsx'
|
||||||
|
|
||||||
|
const root = document.getElementById('root')
|
||||||
|
|
||||||
|
render(() => window.location.protocol === 'file:' ? <Game offline={true} game={new LocalGameStatus(0)}/> : <App />, root!)
|
||||||
345
src/model.tsx
Normal file
345
src/model.tsx
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
import { createStore, produce, type SetStoreFunction } from "solid-js/store";
|
||||||
|
|
||||||
|
type Wall = "horizontal" | "vertical" | null;
|
||||||
|
|
||||||
|
export type Move = {
|
||||||
|
type: "move" | "horizontal" | "vertical",
|
||||||
|
from: Coord,
|
||||||
|
to?: Coord,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type State = {
|
||||||
|
curPlayer: 0 | 1;
|
||||||
|
coords: [Coord, Coord];
|
||||||
|
wallsLeft: [number, number];
|
||||||
|
walls: Wall[][];
|
||||||
|
moves: Move[];
|
||||||
|
winner?: 0 | 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LocalGameStatus {
|
||||||
|
state: State
|
||||||
|
setState: SetStoreFunction<State>
|
||||||
|
|
||||||
|
constructor(arg: 0 | 1 | State) {
|
||||||
|
let state, setState;
|
||||||
|
if (typeof arg === "number") {
|
||||||
|
[state, setState] = createStore({
|
||||||
|
curPlayer: arg,
|
||||||
|
coords: [{ x: 4, y: 0 }, { x: 4, y: 8 }] as [Coord, Coord],
|
||||||
|
wallsLeft: [MAX_WALLS, MAX_WALLS] as [number, number],
|
||||||
|
walls: [...Array(8).keys()].map(() => [...Array(8).keys()].map(() => null as Wall)),
|
||||||
|
moves: [] as Move[]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
[state, setState] = createStore(arg);
|
||||||
|
}
|
||||||
|
this.state = state;
|
||||||
|
this.setState = setState;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(curPlayer: 0 | 1) {
|
||||||
|
this.setState({
|
||||||
|
curPlayer,
|
||||||
|
coords: [{ x: 4, y: 0 }, { x: 4, y: 8 }],
|
||||||
|
wallsLeft: [MAX_WALLS, MAX_WALLS],
|
||||||
|
walls: [...Array(8).keys()].map(() => [...Array(8).keys()].map(() => null as Wall)),
|
||||||
|
moves: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toUrlQuery(): string {
|
||||||
|
return this.state.moves.map(move => {
|
||||||
|
if (move.type === 'move') {
|
||||||
|
const from = move.from;
|
||||||
|
const to = move.to!;
|
||||||
|
return `m${from.x}${from.y}${to.x}${to.y}`;
|
||||||
|
} else {
|
||||||
|
const typeChar = move.type === 'horizontal' ? 'h' : 'v';
|
||||||
|
return `${typeChar}${move.from.x}${move.from.y}`;
|
||||||
|
}
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
move(coord: Coord) {
|
||||||
|
this.setState("moves", this.state.moves.length, { type: "move", from: { ...this.state.coords[this.state.curPlayer] }, to: coord } as Move);
|
||||||
|
this.setState("coords", this.state.curPlayer, coord);
|
||||||
|
if (coord.y === (this.state.curPlayer === 0 ? 8 : 0)) {
|
||||||
|
this.setState("winner", this.state.curPlayer);
|
||||||
|
}
|
||||||
|
this.setState("curPlayer", ((this.state.curPlayer + 1) & 1) as typeof this.state.curPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
placeWall(coord: { x: number, y: number }, orientation: 'horizontal' | 'vertical'): boolean {
|
||||||
|
const { x, y } = coord;
|
||||||
|
const wallsLeft = this.state.wallsLeft[this.state.curPlayer];
|
||||||
|
|
||||||
|
if (wallsLeft <= 0) return false;
|
||||||
|
if (x < 0 || x > 7 || y < 0 || y > 7) return false;
|
||||||
|
if (this.state.walls[y][x] !== null) return false;
|
||||||
|
|
||||||
|
if (orientation === 'horizontal') {
|
||||||
|
if ((x > 0 && this.state.walls[y][x - 1] === 'horizontal') || (x < 7 && this.state.walls[y][x + 1] === 'horizontal')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ((y > 0 && this.state.walls[y - 1][x] === 'vertical') || (y < 7 && this.state.walls[y + 1][x] === 'vertical')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState("walls", y, produce((line) => {
|
||||||
|
line[x] = orientation;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const player0PathExists = this.hasPathToGoal(this.state.coords[0], 1);
|
||||||
|
const player1PathExists = this.hasPathToGoal(this.state.coords[1], 2);
|
||||||
|
|
||||||
|
if (player0PathExists && player1PathExists) {
|
||||||
|
this.setState("moves", this.state.moves.length, { type: orientation, from: coord } as Move);
|
||||||
|
this.setState("wallsLeft", this.state.curPlayer, this.state.wallsLeft[this.state.curPlayer] - 1);
|
||||||
|
this.setState("curPlayer", ((this.state.curPlayer + 1) & 1) as typeof this.state.curPlayer);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.setState("walls", y, produce((line) => {
|
||||||
|
line[x] = null;
|
||||||
|
}))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
let move: Move | undefined;
|
||||||
|
if (this.state.moves.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setState("moves", produce((moves) => move = moves.pop()));
|
||||||
|
const coord = move!.from;
|
||||||
|
switch (move!.type) {
|
||||||
|
case "move":
|
||||||
|
this.setState("coords", (this.state.curPlayer + 1) & 1, coord);
|
||||||
|
this.setState("winner", undefined);
|
||||||
|
break;
|
||||||
|
case "horizontal":
|
||||||
|
case "vertical":
|
||||||
|
this.setState("wallsLeft", (this.state.curPlayer + 1) & 1, this.state.wallsLeft[(this.state.curPlayer + 1) & 1] + 1);
|
||||||
|
this.setState("walls", produce((walls) => walls[coord.y][coord.x] = null));
|
||||||
|
}
|
||||||
|
this.setState("curPlayer", ((this.state.curPlayer + 1) & 1) as typeof this.state.curPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
availableDests(): Coord[] {
|
||||||
|
const moves: Coord[] = [];
|
||||||
|
const thisCoord = this.state.coords[this.state.curPlayer];
|
||||||
|
const otherCoord = this.state.coords[(this.state.curPlayer + 1) & 1];
|
||||||
|
const { x, y } = thisCoord;
|
||||||
|
|
||||||
|
const directions = [
|
||||||
|
{ dx: 0, dy: 1 }, { dx: 0, dy: -1 },
|
||||||
|
{ dx: 1, dy: 0 }, { dx: -1, dy: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { dx, dy } of directions) {
|
||||||
|
const nextX = x + dx;
|
||||||
|
const nextY = y + dy;
|
||||||
|
const nextCoord = { x: nextX, y: nextY } as Coord;
|
||||||
|
|
||||||
|
if (nextX < 0 || nextX > 8 || nextY < 0 || nextY > 8) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isWallBlocking(thisCoord, nextCoord)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOpponent = nextX === otherCoord.x && nextY === otherCoord.y;
|
||||||
|
|
||||||
|
if (!isOpponent) {
|
||||||
|
moves.push(nextCoord);
|
||||||
|
} else {
|
||||||
|
const jumpDirections = [
|
||||||
|
{ jdx: dx, jdy: dy },
|
||||||
|
{ jdx: -dy, jdy: dx },
|
||||||
|
{ jdx: dy, jdy: -dx },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { jdx, jdy } of jumpDirections) {
|
||||||
|
const finalX = otherCoord.x + jdx;
|
||||||
|
const finalY = otherCoord.y + jdy;
|
||||||
|
|
||||||
|
if (finalX < 0 || finalX > 8 || finalY < 0 || finalY > 8) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalCoord = { x: finalX, y: finalY } as Coord;
|
||||||
|
|
||||||
|
if (!this.isWallBlocking(otherCoord, finalCoord)) {
|
||||||
|
moves.push(finalCoord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const uniqueMoves = [...new Map(moves.map(item => [`${item.x},${item.y}`, item])).values()];
|
||||||
|
return uniqueMoves.filter(m => m.x >= 0 && m.x <= 8 && m.y >= 0 && m.y <= 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isWallBlocking(from: Coord, to: Coord): boolean {
|
||||||
|
const { x: fromX, y: fromY } = from;
|
||||||
|
const { x: toX, y: toY } = to;
|
||||||
|
|
||||||
|
if (fromX === toX) {
|
||||||
|
const wallY = Math.min(fromY, toY);
|
||||||
|
if (wallY < 0 || wallY > 7) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (fromX > 0 && this.state.walls[wallY][fromX - 1] === 'horizontal') return true;
|
||||||
|
if (fromX < 8 && this.state.walls[wallY][fromX] === 'horizontal') return true;
|
||||||
|
} else {
|
||||||
|
const wallX = Math.min(fromX, toX);
|
||||||
|
if (wallX < 0 || wallX > 7) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (fromY > 0 && this.state.walls[fromY - 1][wallX] === 'vertical') return true;
|
||||||
|
if (fromY < 8 && this.state.walls[fromY][wallX] === 'vertical') return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasPathToGoal(startCoord: Coord, player: 1 | 2): boolean {
|
||||||
|
const goalY = player === 1 ? 8 : 0;
|
||||||
|
const queue: Coord[] = [startCoord];
|
||||||
|
const visited = new Set<string>([`${startCoord.x},${startCoord.y}`]);
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
|
||||||
|
if (current.y === goalY) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = current;
|
||||||
|
const directions = [
|
||||||
|
{ dx: 0, dy: 1 }, { dx: 0, dy: -1 },
|
||||||
|
{ dx: 1, dy: 0 }, { dx: -1, dy: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { dx, dy } of directions) {
|
||||||
|
const nextX = x + dx;
|
||||||
|
const nextY = y + dy;
|
||||||
|
|
||||||
|
if (nextX < 0 || nextX > 8 || nextY < 0 || nextY > 8) continue;
|
||||||
|
|
||||||
|
const nextCoord = { x: nextX, y: nextY } as Coord;
|
||||||
|
const nextKey = `${nextX},${nextY}`;
|
||||||
|
|
||||||
|
if (visited.has(nextKey) || this.isWallBlocking(current, nextCoord)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(nextKey);
|
||||||
|
queue.push(nextCoord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
availableHorizontalWalls() {
|
||||||
|
function* gen(status: LocalGameStatus) {
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
if (
|
||||||
|
status.state.walls[i][j] === null
|
||||||
|
&& (j + 1 >= 8 || status.state.walls[i][j + 1] !== "horizontal")
|
||||||
|
&& (j - 1 < 0 || status.state.walls[i][j - 1] !== "horizontal")
|
||||||
|
) {
|
||||||
|
yield { x: j, y: i } as Coord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...gen(this)];
|
||||||
|
}
|
||||||
|
|
||||||
|
availableVerticalWalls() {
|
||||||
|
function* gen(status: LocalGameStatus) {
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
if (
|
||||||
|
status.state.walls[i][j] === null
|
||||||
|
&& (i + 1 >= 8 || status.state.walls[i + 1][j] !== "vertical")
|
||||||
|
&& (i - 1 < 0 || status.state.walls[i - 1][j] !== "vertical")
|
||||||
|
) {
|
||||||
|
yield { x: j, y: i } as Coord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...gen(this)];
|
||||||
|
}
|
||||||
|
|
||||||
|
horizontalWalls() {
|
||||||
|
function* gen(status: LocalGameStatus) {
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
if (status.state.walls[i][j] === "horizontal") {
|
||||||
|
yield { x: j, y: i } as Coord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...gen(this)];
|
||||||
|
}
|
||||||
|
|
||||||
|
verticalWalls() {
|
||||||
|
function* gen(status: LocalGameStatus) {
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
if (status.state.walls[i][j] === "vertical") {
|
||||||
|
yield { x: j, y: i } as Coord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...gen(this)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
|
||||||
|
? Acc[number]
|
||||||
|
: Enumerate<N, [...Acc, Acc['length']]>
|
||||||
|
|
||||||
|
type Range<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>
|
||||||
|
|
||||||
|
export type Coord = {
|
||||||
|
x: Range<0, 9>,
|
||||||
|
y: Range<0, 9>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MAX_WALLS = 10
|
||||||
|
|
||||||
|
export type Action = "move" | "horizontal" | "vertical"
|
||||||
|
|
||||||
|
export function gameStatusFromUrl(movesStr: string): LocalGameStatus {
|
||||||
|
const game = new LocalGameStatus(0);
|
||||||
|
let i = 0;
|
||||||
|
while (i < movesStr.length) {
|
||||||
|
const type = movesStr[i];
|
||||||
|
if (type === 'm') {
|
||||||
|
const toX = parseInt(movesStr[i + 3], 10);
|
||||||
|
const toY = parseInt(movesStr[i + 4], 10);
|
||||||
|
if (isNaN(toX) || isNaN(toY)) break;
|
||||||
|
game.move({ x: toX, y: toY } as Coord);
|
||||||
|
i += 5;
|
||||||
|
} else if (type === 'h' || type === 'v') {
|
||||||
|
const x = parseInt(movesStr[i + 1], 10);
|
||||||
|
const y = parseInt(movesStr[i + 2], 10);
|
||||||
|
if (isNaN(x) || isNaN(y)) break;
|
||||||
|
const orientation = type === 'h' ? 'horizontal' : 'vertical';
|
||||||
|
game.placeWall({ x, y }, orientation);
|
||||||
|
i += 3;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return game;
|
||||||
|
}
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
12
tailwind.config.js
Normal file
12
tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
28
tsconfig.app.json
Normal file
28
tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import solid from 'vite-plugin-solid'
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solid(), viteSingleFile()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user