@scriptonita/chess-football-ui 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -3
- package/dist/{chunk-LTFNSTAQ.cjs → chunk-BSE4DUXM.cjs} +18 -5
- package/dist/chunk-BSE4DUXM.cjs.map +1 -0
- package/dist/{chunk-N24LGLLO.js → chunk-C5HCN7O5.js} +18 -6
- package/dist/chunk-C5HCN7O5.js.map +1 -0
- package/dist/index.cjs +397 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +171 -8
- package/dist/index.d.ts +171 -8
- package/dist/index.js +390 -88
- package/dist/index.js.map +1 -1
- package/dist/store.cjs +7 -3
- package/dist/store.d.cts +9 -1
- package/dist/store.d.ts +9 -1
- package/dist/store.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-LTFNSTAQ.cjs.map +0 -1
- package/dist/chunk-N24LGLLO.js.map +0 -1
package/README.md
CHANGED
|
@@ -52,9 +52,59 @@ expect(missing).toEqual([])
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
**2. Design tokens.** Components emit Tailwind classes bound to tokens the app defines. A
|
|
55
|
-
token that is missing fails silently — the rule
|
|
56
|
-
`--last-move-highlight
|
|
57
|
-
|
|
55
|
+
token that is missing fails silently — Tailwind emits the rule, the variable resolves to
|
|
56
|
+
nothing, and the declaration is dropped. That is how `--last-move-highlight` went missing
|
|
57
|
+
in the CrazyGames SPA: the last-move highlight and the bot's turn replay never rendered,
|
|
58
|
+
with no error anywhere. `REQUIRED_TOKENS` is the manifest; assert it the same way:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { REQUIRED_TOKENS } from '@scriptonita/chess-football-ui'
|
|
62
|
+
|
|
63
|
+
const css = readFileSync('src/index.css', 'utf8')
|
|
64
|
+
expect(REQUIRED_TOKENS.filter(t => !css.includes(`${t}:`))).toEqual([])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`--field-frame` is new in 0.9.0: the board frame was hardcoded `#1a3317` in three places,
|
|
68
|
+
hand-tuned for the webapp's grass and visibly wrong against the CrazyGames pitch.
|
|
69
|
+
|
|
70
|
+
`--ball-color` joins the contract in 0.10.0: the ball was a hardcoded sky blue, the same
|
|
71
|
+
hue as `--pass-highlight`, while both apps defined a `--ball-color` nobody read.
|
|
72
|
+
|
|
73
|
+
## Bot turn
|
|
74
|
+
|
|
75
|
+
`useBotTurn` owns a bot's turn end to end — entry gates (turn, pending goal, match over),
|
|
76
|
+
timed replay, cancellation on unmount, error + retry, and skip. The app only says where the
|
|
77
|
+
turn comes from, so the same hook serves a local search and a server call:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { useBotTurn } from '@scriptonita/chess-football-ui'
|
|
81
|
+
import { playBotTurn } from '@scriptonita/chess-football-engine'
|
|
82
|
+
|
|
83
|
+
const { botThinking, error, retry, skip } = useBotTurn({
|
|
84
|
+
boardState, botSide,
|
|
85
|
+
getTurn: (board, side) => playBotTurn(bot, board, side), // local
|
|
86
|
+
// getTurn: (board) => fetch('/api/turn', …).then(r => r.json()), // server
|
|
87
|
+
onApplyState: setBoardState,
|
|
88
|
+
matchOver: !!matchResult,
|
|
89
|
+
goalPending: !!goalScored,
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Pacing is fixed in one place: `BOT_TURN_START_DELAY_MS` (400) before the first action,
|
|
94
|
+
`BOT_TURN_STEP_MS` (900) between actions and `BOT_TURN_SETTLE_MS` (500) for the last one to
|
|
95
|
+
land. The closing end-of-turn state takes no step of its own, so a full 5-AP turn lasts 4.5 s.
|
|
96
|
+
|
|
97
|
+
## First-match hints
|
|
98
|
+
|
|
99
|
+
`<GameHints userSide seen onSeen />` shows one dismissible sentence the first time each
|
|
100
|
+
situation happens (`GAME_HINT_IDS`: first turn, selecting a piece, holding the ball, last
|
|
101
|
+
action point). The texts are `hints.*` keys of `GAME_I18N_KEYS`. The app decides when to mount
|
|
102
|
+
it and where `seen` is stored.
|
|
103
|
+
|
|
104
|
+
## Forgiving taps
|
|
105
|
+
|
|
106
|
+
`<GameBoard forgivingTaps />` resolves a touch on an empty square that borders exactly one
|
|
107
|
+
legal target to that target. Opt-in, touch only, never when the tap is ambiguous.
|
|
58
108
|
|
|
59
109
|
## Peer dependencies
|
|
60
110
|
|
|
@@ -4,19 +4,31 @@ var zustand = require('zustand');
|
|
|
4
4
|
var chessFootballEngine = require('@scriptonita/chess-football-engine');
|
|
5
5
|
|
|
6
6
|
// src/store/use-game-store.ts
|
|
7
|
-
var
|
|
7
|
+
var DEFAULT_MAX_AP = 5;
|
|
8
|
+
var INITIAL = {
|
|
8
9
|
boardState: null,
|
|
9
10
|
selectedPieceId: null,
|
|
10
|
-
interactionMode: null
|
|
11
|
+
interactionMode: null
|
|
12
|
+
};
|
|
13
|
+
var useGameStore = zustand.create((set) => ({
|
|
14
|
+
...INITIAL,
|
|
11
15
|
setBoardState: (state) => set({ boardState: state }),
|
|
12
16
|
setSelectedPieceId: (id) => set({ selectedPieceId: id }),
|
|
13
17
|
setInteractionMode: (mode) => set({ interactionMode: mode }),
|
|
18
|
+
/**
|
|
19
|
+
* The store is a module-global singleton, so a new match opens on the
|
|
20
|
+
* previous match's final board — which ended on a goal, and was therefore
|
|
21
|
+
* re-detected as a fresh goal (the bug behind webapp PR #42). Apps were
|
|
22
|
+
* each reaching for `useGameStore.setState({...})` by hand to work around
|
|
23
|
+
* it; this makes the intent explicit and keeps the shape in one place.
|
|
24
|
+
*/
|
|
25
|
+
reset: () => set({ ...INITIAL }),
|
|
14
26
|
resetTurn: () => set((state) => {
|
|
15
27
|
if (!state.boardState) return state;
|
|
16
28
|
return {
|
|
17
29
|
boardState: {
|
|
18
30
|
...state.boardState,
|
|
19
|
-
actionPoints: state.boardState.maxActionPoints ??
|
|
31
|
+
actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,
|
|
20
32
|
pieces: state.boardState.pieces.map((p) => ({ ...p, hasMovedThisTurn: false }))
|
|
21
33
|
}
|
|
22
34
|
};
|
|
@@ -50,6 +62,7 @@ Object.defineProperty(exports, "getInitialBoardState", {
|
|
|
50
62
|
enumerable: true,
|
|
51
63
|
get: function () { return chessFootballEngine.getInitialBoardState; }
|
|
52
64
|
});
|
|
65
|
+
exports.DEFAULT_MAX_AP = DEFAULT_MAX_AP;
|
|
53
66
|
exports.useGameStore = useGameStore;
|
|
54
|
-
//# sourceMappingURL=chunk-
|
|
55
|
-
//# sourceMappingURL=chunk-
|
|
67
|
+
//# sourceMappingURL=chunk-BSE4DUXM.cjs.map
|
|
68
|
+
//# sourceMappingURL=chunk-BSE4DUXM.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/use-game-store.ts"],"names":["create","applyMove","applyPass","applyEndTurn"],"mappings":";;;;;;AAaO,IAAM,cAAA,GAAiB;AAiB9B,IAAM,OAAA,GAAU;AAAA,EACZ,UAAA,EAAY,IAAA;AAAA,EACZ,eAAA,EAAiB,IAAA;AAAA,EACjB,eAAA,EAAiB;AACrB,CAAA;AAEO,IAAM,YAAA,GAAeA,cAAA,CAAkB,CAAC,GAAA,MAAS;AAAA,EACpD,GAAG,OAAA;AAAA,EAEH,eAAe,CAAC,KAAA,KAAU,IAAI,EAAE,UAAA,EAAY,OAAO,CAAA;AAAA,EACnD,oBAAoB,CAAC,EAAA,KAAO,IAAI,EAAE,eAAA,EAAiB,IAAI,CAAA;AAAA,EACvD,oBAAoB,CAAC,IAAA,KAAS,IAAI,EAAE,eAAA,EAAiB,MAAM,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS3D,OAAO,MAAM,GAAA,CAAI,EAAE,GAAG,SAAS,CAAA;AAAA,EAE/B,SAAA,EAAW,MAAM,GAAA,CAAI,CAAC,KAAA,KAAU;AAC5B,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,EAAY,OAAO,KAAA;AAC9B,IAAA,OAAO;AAAA,MACH,UAAA,EAAY;AAAA,QACR,GAAG,KAAA,CAAM,UAAA;AAAA,QACT,YAAA,EAAc,KAAA,CAAM,UAAA,CAAW,eAAA,IAAmB,cAAA;AAAA,QAClD,MAAA,EAAQ,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,GAAA,CAAI,CAAA,CAAA,MAAM,EAAE,GAAG,CAAA,EAAG,gBAAA,EAAkB,KAAA,EAAM,CAAE;AAAA;AAChF,KACJ;AAAA,EACJ,CAAC,CAAA;AAAA,EAED,WAAW,CAAC,OAAA,EAAS,EAAA,KAAO,GAAA,CAAI,CAAC,KAAA,KAAU;AACvC,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,IAAc,MAAM,UAAA,CAAW,YAAA,IAAgB,GAAG,OAAO,KAAA;AACpE,IAAA,MAAM,KAAA,GAAQ,MAAM,UAAA,CAAW,MAAA,CAAO,KAAK,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,OAAO,CAAA;AAChE,IAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,gBAAA,EAAkB,OAAO,KAAA;AAE7C,IAAA,MAAM,EAAE,UAAA,EAAW,GAAIC,8BAAU,KAAA,CAAM,UAAA,EAAY,SAAS,EAAE,CAAA;AAC9D,IAAA,OAAO,EAAE,UAAA,EAAY,eAAA,EAAiB,IAAA,EAAM,iBAAiB,IAAA,EAAK;AAAA,EACtE,CAAC,CAAA;AAAA,EAED,QAAA,EAAU,CAAC,EAAA,KAAO,GAAA,CAAI,CAAC,KAAA,KAAU;AAC7B,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,IAAc,KAAA,CAAM,UAAA,CAAW,YAAA,IAAgB,CAAA,IAAK,CAAC,KAAA,CAAM,UAAA,CAAW,IAAA,CAAK,QAAA,EAAU,OAAO,KAAA;AACvG,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,EAAA,KAAO,KAAA,CAAM,UAAA,CAAY,IAAA,CAAK,QAAQ,CAAA;AACzF,IAAA,IAAI,CAAC,QAAQ,OAAO,KAAA;AAEpB,IAAA,MAAM,EAAE,UAAA,EAAY,aAAA,KAAkBC,6BAAA,CAAU,KAAA,CAAM,YAAY,EAAE,CAAA;AACpE,IAAA,MAAM,aAAA,GAAgB,CAAC,aAAA,IAAiB,UAAA,CAAW,IAAA,CAAK,QAAA;AACxD,IAAA,OAAO;AAAA,MACH,UAAA;AAAA,MACA,eAAA,EAAiB,aAAA,GAAgB,UAAA,CAAW,IAAA,CAAK,QAAA,GAAW,IAAA;AAAA,MAC5D,eAAA,EAAiB,gBAAgB,MAAA,GAAS;AAAA,KAC9C;AAAA,EACJ,CAAC,CAAA;AAAA,EAED,OAAA,EAAS,MAAM,GAAA,CAAI,CAAC,KAAA,KAAU;AAC1B,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,EAAY,OAAO,KAAA;AAC9B,IAAA,OAAO,EAAE,UAAA,EAAYC,gCAAA,CAAa,KAAA,CAAM,UAAU,CAAA,EAAE;AAAA,EACxD,CAAC;AACL,CAAA,CAAE","file":"chunk-BSE4DUXM.cjs","sourcesContent":["import { create } from 'zustand'\nimport type { BoardState, Position } from '@scriptonita/chess-football-engine'\nimport { applyMove, applyPass, applyEndTurn } from '@scriptonita/chess-football-engine'\n\n// The canonical kickoff position lives in the engine (rules-adjacent). Re-exported\n// here so app imports from `@scriptonita/chess-football-ui/store` keep working.\nexport { getInitialBoardState } from '@scriptonita/chess-football-engine'\n\n/**\n * Fallback when a board predates configurable action points. `maxActionPoints`\n * is optional on `BoardState`, and every consumer was inventing its own `?? 5`\n * — except `TurnBanner`, which had none and rendered \"3/\" on an old match.\n */\nexport const DEFAULT_MAX_AP = 5\n\ninterface GameStore {\n boardState: BoardState | null\n selectedPieceId: string | null\n interactionMode: 'move' | 'pass' | null\n setBoardState: (state: BoardState) => void\n setSelectedPieceId: (id: string | null) => void\n setInteractionMode: (mode: 'move' | 'pass' | null) => void\n resetTurn: () => void\n /** Return the store to its initial state. */\n reset: () => void\n movePiece: (pieceId: string, to: Position) => void\n passBall: (to: Position) => void\n endTurn: () => void\n}\n\nconst INITIAL = {\n boardState: null,\n selectedPieceId: null,\n interactionMode: null,\n} satisfies Pick<GameStore, 'boardState' | 'selectedPieceId' | 'interactionMode'>\n\nexport const useGameStore = create<GameStore>((set) => ({\n ...INITIAL,\n\n setBoardState: (state) => set({ boardState: state }),\n setSelectedPieceId: (id) => set({ selectedPieceId: id }),\n setInteractionMode: (mode) => set({ interactionMode: mode }),\n\n /**\n * The store is a module-global singleton, so a new match opens on the\n * previous match's final board — which ended on a goal, and was therefore\n * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were\n * each reaching for `useGameStore.setState({...})` by hand to work around\n * it; this makes the intent explicit and keeps the shape in one place.\n */\n reset: () => set({ ...INITIAL }),\n\n resetTurn: () => set((state) => {\n if (!state.boardState) return state\n return {\n boardState: {\n ...state.boardState,\n actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,\n pieces: state.boardState.pieces.map(p => ({ ...p, hasMovedThisTurn: false })),\n },\n }\n }),\n\n movePiece: (pieceId, to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0) return state\n const piece = state.boardState.pieces.find(p => p.id === pieceId)\n if (!piece || piece.hasMovedThisTurn) return state\n\n const { boardState } = applyMove(state.boardState, pieceId, to)\n return { boardState, selectedPieceId: null, interactionMode: null }\n }),\n\n passBall: (to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0 || !state.boardState.ball.holderId) return state\n const holder = state.boardState.pieces.find(p => p.id === state.boardState!.ball.holderId)\n if (!holder) return state\n\n const { boardState, forcedTurnEnd } = applyPass(state.boardState, to)\n const keepSelection = !forcedTurnEnd && boardState.ball.holderId\n return {\n boardState,\n selectedPieceId: keepSelection ? boardState.ball.holderId : null,\n interactionMode: keepSelection ? 'pass' : null,\n }\n }),\n\n endTurn: () => set((state) => {\n if (!state.boardState) return state\n return { boardState: applyEndTurn(state.boardState) }\n }),\n}))\n"]}
|
|
@@ -3,19 +3,31 @@ import { applyEndTurn, applyPass, applyMove } from '@scriptonita/chess-football-
|
|
|
3
3
|
export { getInitialBoardState } from '@scriptonita/chess-football-engine';
|
|
4
4
|
|
|
5
5
|
// src/store/use-game-store.ts
|
|
6
|
-
var
|
|
6
|
+
var DEFAULT_MAX_AP = 5;
|
|
7
|
+
var INITIAL = {
|
|
7
8
|
boardState: null,
|
|
8
9
|
selectedPieceId: null,
|
|
9
|
-
interactionMode: null
|
|
10
|
+
interactionMode: null
|
|
11
|
+
};
|
|
12
|
+
var useGameStore = create((set) => ({
|
|
13
|
+
...INITIAL,
|
|
10
14
|
setBoardState: (state) => set({ boardState: state }),
|
|
11
15
|
setSelectedPieceId: (id) => set({ selectedPieceId: id }),
|
|
12
16
|
setInteractionMode: (mode) => set({ interactionMode: mode }),
|
|
17
|
+
/**
|
|
18
|
+
* The store is a module-global singleton, so a new match opens on the
|
|
19
|
+
* previous match's final board — which ended on a goal, and was therefore
|
|
20
|
+
* re-detected as a fresh goal (the bug behind webapp PR #42). Apps were
|
|
21
|
+
* each reaching for `useGameStore.setState({...})` by hand to work around
|
|
22
|
+
* it; this makes the intent explicit and keeps the shape in one place.
|
|
23
|
+
*/
|
|
24
|
+
reset: () => set({ ...INITIAL }),
|
|
13
25
|
resetTurn: () => set((state) => {
|
|
14
26
|
if (!state.boardState) return state;
|
|
15
27
|
return {
|
|
16
28
|
boardState: {
|
|
17
29
|
...state.boardState,
|
|
18
|
-
actionPoints: state.boardState.maxActionPoints ??
|
|
30
|
+
actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,
|
|
19
31
|
pieces: state.boardState.pieces.map((p) => ({ ...p, hasMovedThisTurn: false }))
|
|
20
32
|
}
|
|
21
33
|
};
|
|
@@ -45,6 +57,6 @@ var useGameStore = create((set) => ({
|
|
|
45
57
|
})
|
|
46
58
|
}));
|
|
47
59
|
|
|
48
|
-
export { useGameStore };
|
|
49
|
-
//# sourceMappingURL=chunk-
|
|
50
|
-
//# sourceMappingURL=chunk-
|
|
60
|
+
export { DEFAULT_MAX_AP, useGameStore };
|
|
61
|
+
//# sourceMappingURL=chunk-C5HCN7O5.js.map
|
|
62
|
+
//# sourceMappingURL=chunk-C5HCN7O5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/use-game-store.ts"],"names":[],"mappings":"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","file":"chunk-C5HCN7O5.js","sourcesContent":["import { create } from 'zustand'\nimport type { BoardState, Position } from '@scriptonita/chess-football-engine'\nimport { applyMove, applyPass, applyEndTurn } from '@scriptonita/chess-football-engine'\n\n// The canonical kickoff position lives in the engine (rules-adjacent). Re-exported\n// here so app imports from `@scriptonita/chess-football-ui/store` keep working.\nexport { getInitialBoardState } from '@scriptonita/chess-football-engine'\n\n/**\n * Fallback when a board predates configurable action points. `maxActionPoints`\n * is optional on `BoardState`, and every consumer was inventing its own `?? 5`\n * — except `TurnBanner`, which had none and rendered \"3/\" on an old match.\n */\nexport const DEFAULT_MAX_AP = 5\n\ninterface GameStore {\n boardState: BoardState | null\n selectedPieceId: string | null\n interactionMode: 'move' | 'pass' | null\n setBoardState: (state: BoardState) => void\n setSelectedPieceId: (id: string | null) => void\n setInteractionMode: (mode: 'move' | 'pass' | null) => void\n resetTurn: () => void\n /** Return the store to its initial state. */\n reset: () => void\n movePiece: (pieceId: string, to: Position) => void\n passBall: (to: Position) => void\n endTurn: () => void\n}\n\nconst INITIAL = {\n boardState: null,\n selectedPieceId: null,\n interactionMode: null,\n} satisfies Pick<GameStore, 'boardState' | 'selectedPieceId' | 'interactionMode'>\n\nexport const useGameStore = create<GameStore>((set) => ({\n ...INITIAL,\n\n setBoardState: (state) => set({ boardState: state }),\n setSelectedPieceId: (id) => set({ selectedPieceId: id }),\n setInteractionMode: (mode) => set({ interactionMode: mode }),\n\n /**\n * The store is a module-global singleton, so a new match opens on the\n * previous match's final board — which ended on a goal, and was therefore\n * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were\n * each reaching for `useGameStore.setState({...})` by hand to work around\n * it; this makes the intent explicit and keeps the shape in one place.\n */\n reset: () => set({ ...INITIAL }),\n\n resetTurn: () => set((state) => {\n if (!state.boardState) return state\n return {\n boardState: {\n ...state.boardState,\n actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,\n pieces: state.boardState.pieces.map(p => ({ ...p, hasMovedThisTurn: false })),\n },\n }\n }),\n\n movePiece: (pieceId, to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0) return state\n const piece = state.boardState.pieces.find(p => p.id === pieceId)\n if (!piece || piece.hasMovedThisTurn) return state\n\n const { boardState } = applyMove(state.boardState, pieceId, to)\n return { boardState, selectedPieceId: null, interactionMode: null }\n }),\n\n passBall: (to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0 || !state.boardState.ball.holderId) return state\n const holder = state.boardState.pieces.find(p => p.id === state.boardState!.ball.holderId)\n if (!holder) return state\n\n const { boardState, forcedTurnEnd } = applyPass(state.boardState, to)\n const keepSelection = !forcedTurnEnd && boardState.ball.holderId\n return {\n boardState,\n selectedPieceId: keepSelection ? boardState.ball.holderId : null,\n interactionMode: keepSelection ? 'pass' : null,\n }\n }),\n\n endTurn: () => set((state) => {\n if (!state.boardState) return state\n return { boardState: applyEndTurn(state.boardState) }\n }),\n}))\n"]}
|