@xavatech/create-xara-game 1.0.0 → 1.0.2
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 +4 -6
- package/index.mjs +12 -10
- package/package.json +2 -2
- package/template/.cursor/skills/build-xara-game/SKILL.md +3 -3
- package/template/README.md +4 -4
- package/template/index.html +1 -10
- package/template/package.json +1 -1
- package/template/src/main.ts +6 -97
- package/template/src/style.css +5 -121
- package/template/vite.config.ts +1 -0
- package/template/src/rules.test.ts +0 -22
- package/template/src/rules.ts +0 -20
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @xavatech/create-xara-game
|
|
2
2
|
|
|
3
|
-
Starts a game for Xara chats:
|
|
3
|
+
Starts a game for Xara chats: an empty page built with [`@xavatech/xara-game-sdk`](https://www.npmjs.com/package/@xavatech/xara-game-sdk), and with the [game skill](https://docs.usexara.ai/games/skill) installed, so a coding assistant can turn your description into the real game. Outside the app, a bar switches which player's phone the page is.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @xavatech/create-xara-game word-chain --name "Word Chain" --players 2-4
|
|
@@ -13,19 +13,17 @@ npm run dev
|
|
|
13
13
|
| `--name` | The game's name, as players see it. Defaults to the folder's name. |
|
|
14
14
|
| `--description` | One line about the game. |
|
|
15
15
|
| `--players` | `2`, or a range like `2-4`. Defaults to `2`. |
|
|
16
|
-
| `--xara-games <dir>` | For Xara's own games.
|
|
17
|
-
|
|
18
|
-
The starter is a placeholder game: add 1, 2, or 3, and whoever makes the total 10 wins.
|
|
16
|
+
| `--xara-games <dir>` | For Xara's own games. Creates the game inside that [xara-games](https://github.com/xavatech/xara-games) checkout instead of the current folder. |
|
|
19
17
|
|
|
20
18
|
## Xara's own games
|
|
21
19
|
|
|
22
20
|
Name the folder `xara-<game>` and pass `--xara-games`:
|
|
23
21
|
|
|
24
22
|
```bash
|
|
25
|
-
npx @xavatech/create-xara-game xara-word-chain --name "Word Chain" --players 2-4 --xara-games
|
|
23
|
+
npx @xavatech/create-xara-game xara-word-chain --name "Word Chain" --players 2-4 --xara-games path/to/xara-games
|
|
26
24
|
```
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
Run it from any folder, including inside `xara-games` with `--xara-games .`. The game is created at `games/word-chain` in that checkout and registered there as a submodule whose remote is `github.com/xavatech/xara-word-chain`. Nothing is created in the folder you run it from. It is added to `games.json` and built into `site/word-chain/`. With the site served at `GAMES_URL`, run the backend's `Game` seeder to list it. Then create the GitHub repo and push.
|
|
29
27
|
|
|
30
28
|
## Change the starter
|
|
31
29
|
|
package/index.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const USAGE = `Start a game for Xara chats.
|
|
|
12
12
|
--name <name> The game's name, as players see it. Defaults to the folder's name.
|
|
13
13
|
--description <description> One line about the game.
|
|
14
14
|
--players <players> How many play: 2, or a range like 2-4. Defaults to 2.
|
|
15
|
-
--xara-games <dir> For Xara's own games:
|
|
15
|
+
--xara-games <dir> For Xara's own games: create it in that xara-games checkout, at games/<game>.`;
|
|
16
16
|
|
|
17
17
|
const fail = (message) => {
|
|
18
18
|
console.error(`\n${message}\n`);
|
|
@@ -34,13 +34,15 @@ if (values.help || !positionals[0]) {
|
|
|
34
34
|
process.exit(values.help ? 0 : 1);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const
|
|
38
|
-
const folder = basename(dir);
|
|
37
|
+
const folder = basename(positionals[0]);
|
|
39
38
|
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(folder)) fail("Name the folder like word-chain: lowercase letters, numbers, and dashes.");
|
|
40
|
-
if (existsSync(dir)) fail(`${dir} already exists.`);
|
|
41
39
|
|
|
42
|
-
// Xara's own repos are named xara-<game>; the game is served at /<game>/.
|
|
40
|
+
// Xara's own repos are named xara-<game>; the game lives in xara-games at games/<game> and is served at /<game>/.
|
|
43
41
|
const path = folder.replace(/^xara-/, "");
|
|
42
|
+
const gamesDir = values["xara-games"] && resolve(values["xara-games"]);
|
|
43
|
+
const dir = gamesDir ? join(gamesDir, "games", path) : resolve(positionals[0]);
|
|
44
|
+
const repoUrl = `https://github.com/xavatech/${folder}.git`;
|
|
45
|
+
if (existsSync(dir)) fail(`${dir} already exists.`);
|
|
44
46
|
const name = values.name?.trim() || path.split("-").map((word) => word[0].toUpperCase() + word.slice(1)).join(" ");
|
|
45
47
|
const description = values.description?.trim() ?? "";
|
|
46
48
|
const [minPlayers, maxPlayers = minPlayers] = values.players.split("-").map(Number);
|
|
@@ -48,7 +50,6 @@ if (![minPlayers, maxPlayers].every(Number.isInteger) || minPlayers < 2 || maxPl
|
|
|
48
50
|
fail("--players is from 2 to 8, like 2 or 2-4.");
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
const gamesDir = values["xara-games"] && resolve(values["xara-games"]);
|
|
52
53
|
const listFile = gamesDir && join(gamesDir, "games.json");
|
|
53
54
|
const list = listFile && (existsSync(listFile) ? JSON.parse(readFileSync(listFile, "utf8")) : fail(`${listFile} was not found.`));
|
|
54
55
|
if (list?.some((game) => game.path === path || game.name === name)) fail(`${path} or "${name}" is already in games.json.`);
|
|
@@ -75,8 +76,9 @@ run("npm install --no-fund --no-audit", dir);
|
|
|
75
76
|
run(`git init -q -b main && git add -A && git commit -q -m "${name.replace(/"/g, "")} for Xara chats"`, dir);
|
|
76
77
|
|
|
77
78
|
if (gamesDir) {
|
|
78
|
-
|
|
79
|
-
run(`git
|
|
79
|
+
// The game is already at games/<game>, so this registers that repo in place instead of cloning it.
|
|
80
|
+
run(`git remote add origin ${repoUrl}`, dir);
|
|
81
|
+
run(`git submodule add -q ${repoUrl} games/${path}`, gamesDir);
|
|
80
82
|
writeFileSync(listFile, `${JSON.stringify([...list, { path, name, description, minPlayers, maxPlayers }], null, 2)}\n`);
|
|
81
83
|
run(`node build.mjs ${path}`, gamesDir);
|
|
82
84
|
run("git add -A", gamesDir);
|
|
@@ -85,7 +87,7 @@ if (gamesDir) {
|
|
|
85
87
|
console.log(`
|
|
86
88
|
${name} is ready.
|
|
87
89
|
|
|
88
|
-
cd ${
|
|
90
|
+
cd ${dir}
|
|
89
91
|
npm run dev Play it in a browser; every player shares the screen.
|
|
90
92
|
|
|
91
93
|
Then describe the real game to your coding assistant in that folder.`);
|
|
@@ -96,7 +98,7 @@ It is in xara-games at games/${path}, and built into site/${path}/.
|
|
|
96
98
|
|
|
97
99
|
List it: bun run avanda.ts app seed -t Game (in xara_backend, with xara-games served at GAMES_URL)
|
|
98
100
|
Push it: create github.com/xavatech/${folder}, then
|
|
99
|
-
cd ${dir} && git
|
|
101
|
+
cd ${dir} && git push -u origin main
|
|
100
102
|
cd ${gamesDir} && git commit -m "Add ${name}"`);
|
|
101
103
|
} else {
|
|
102
104
|
console.log(`To ship it, see README.md.`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xavatech/create-xara-game",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Start a game for Xara chats:
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Start a game for Xara chats: an empty page with the Xara game SDK and the coding-assistant skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-xara-game": "index.mjs"
|
|
@@ -55,7 +55,7 @@ No other origins, no CDN fonts, no iframes.
|
|
|
55
55
|
```ts
|
|
56
56
|
import { connect, inXara, mockApp } from "@xavatech/xara-game-sdk";
|
|
57
57
|
|
|
58
|
-
if (
|
|
58
|
+
if (!inXara()) {
|
|
59
59
|
mockApp({
|
|
60
60
|
players: [
|
|
61
61
|
{ id: 1, name: "Ada" },
|
|
@@ -71,7 +71,7 @@ xara.onChange(draw);
|
|
|
71
71
|
draw(await xara.load());
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
`mockApp`
|
|
74
|
+
`mockApp` runs only outside the app. It adds a bar that switches which player's phone the page is, and a move only goes through on that player's turn. Inside the app it never runs.
|
|
75
75
|
|
|
76
76
|
## Draw and play
|
|
77
77
|
|
|
@@ -130,6 +130,6 @@ Use the names above. These older names are not the SDK: `ready`, `onState`, `vie
|
|
|
130
130
|
|
|
131
131
|
## What to tell the user
|
|
132
132
|
|
|
133
|
-
The built `dist/` is a static site. It has to be served over https on one origin. They submit that URL in the Xara business dashboard with the player counts and SDK version `2.
|
|
133
|
+
The built `dist/` is a static site. It has to be served over https on one origin. They submit that URL in the Xara business dashboard with the player counts and SDK version `2.1.0`. The game stays in review until an admin approves it. Changing the URL or the player counts after approval sends it back to review. Changing the name, description, or icon does not.
|
|
134
134
|
|
|
135
135
|
Do not submit, host, or publish the package unless the user asks.
|
package/template/README.md
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
__DESCRIPTION__
|
|
4
4
|
|
|
5
|
-
A game people start from a Xara chat, built with [`@xavatech/xara-game-sdk`](https://www.npmjs.com/package/@xavatech/xara-game-sdk).
|
|
5
|
+
A game people start from a Xara chat, built with [`@xavatech/xara-game-sdk`](https://www.npmjs.com/package/@xavatech/xara-game-sdk). The page starts empty. Outside the Xara app, a bar at the bottom switches which player's phone it is. Describe the real game to your coding assistant. The skill in `.cursor/skills/build-xara-game` tells it how.
|
|
6
6
|
|
|
7
7
|
## Run it
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm run dev # http://localhost:5173
|
|
11
|
-
npm test
|
|
10
|
+
npm run dev # http://localhost:5173
|
|
11
|
+
npm test
|
|
12
12
|
npm run build # static files in dist/
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Ship it
|
|
16
16
|
|
|
17
17
|
1. Serve `dist/` over **https**. Xara only opens https games, and only answers SDK calls from that URL's origin.
|
|
18
|
-
2. In the Xara business dashboard, open **Games → Submit a game** with the URL, __PLAYERS__ players, and SDK version `2.
|
|
18
|
+
2. In the Xara business dashboard, open **Games → Submit a game** with the URL, __PLAYERS__ players, and SDK version `2.1.0`.
|
|
19
19
|
3. Once an admin approves it, people find it under **+ → Game** in their chats.
|
|
20
20
|
|
|
21
21
|
See [Games overview](https://docs.usexara.ai/games/overview).
|
package/template/index.html
CHANGED
|
@@ -7,16 +7,7 @@
|
|
|
7
7
|
<title>__NAME__</title>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
|
-
<
|
|
11
|
-
<ol id="players" class="players" aria-label="Players"></ol>
|
|
12
|
-
<p id="status" class="status" aria-live="polite">Loading…</p>
|
|
13
|
-
<p id="total" class="total" aria-label="Total">0</p>
|
|
14
|
-
<div id="pad" class="pad" role="group" aria-label="Add to the total"></div>
|
|
15
|
-
<div class="actions">
|
|
16
|
-
<button id="accept" class="action" tabindex="-1">Accept and play</button>
|
|
17
|
-
</div>
|
|
18
|
-
<p id="note" class="note" role="alert"></p>
|
|
19
|
-
</main>
|
|
10
|
+
<p id="status" class="status" role="alert"></p>
|
|
20
11
|
<script type="module" src="./src/main.ts"></script>
|
|
21
12
|
</body>
|
|
22
13
|
</html>
|
package/template/package.json
CHANGED
package/template/src/main.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import "./style.css";
|
|
2
|
-
import { connect, inXara, mockApp, XaraGameError
|
|
3
|
-
import { STEPS, TARGET, canAdd, isWon, totalOf, type Move } from "./rules";
|
|
2
|
+
import { connect, inXara, mockApp, XaraGameError } from "@xavatech/xara-game-sdk";
|
|
4
3
|
|
|
5
|
-
//
|
|
6
|
-
if (
|
|
4
|
+
// Outside the app, a bar switches which player's phone this page is.
|
|
5
|
+
if (!inXara()) {
|
|
7
6
|
mockApp({
|
|
8
7
|
game: { id: 1, name: "__NAME__" },
|
|
9
8
|
players: [
|
|
@@ -14,103 +13,13 @@ if (import.meta.env.DEV && !inXara()) {
|
|
|
14
13
|
});
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const xara = connect<Move>();
|
|
20
|
-
const playersEl = document.getElementById("players")!;
|
|
16
|
+
const xara = connect();
|
|
21
17
|
const statusEl = document.getElementById("status")!;
|
|
22
|
-
const totalEl = document.getElementById("total")!;
|
|
23
|
-
const padEl = document.getElementById("pad")!;
|
|
24
|
-
const acceptEl = document.getElementById("accept") as HTMLButtonElement;
|
|
25
|
-
const noteEl = document.getElementById("note")!;
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
button.className = "key";
|
|
30
|
-
button.textContent = `+${add}`;
|
|
31
|
-
button.disabled = true;
|
|
32
|
-
button.addEventListener("click", () => play(add));
|
|
33
|
-
padEl.append(button);
|
|
34
|
-
return button;
|
|
19
|
+
xara.onChange(() => {
|
|
20
|
+
statusEl.textContent = "";
|
|
35
21
|
});
|
|
36
22
|
|
|
37
|
-
let busy = false;
|
|
38
|
-
|
|
39
|
-
const nameOf = (state: State, playerId: number | null) =>
|
|
40
|
-
state.players.find((player) => player.id === playerId)?.name ?? "Someone";
|
|
41
|
-
|
|
42
|
-
function statusText(state: State): string {
|
|
43
|
-
const { session, me } = state;
|
|
44
|
-
switch (session.status) {
|
|
45
|
-
case "invited":
|
|
46
|
-
return state.players.find((player) => player.you)?.status === "invited"
|
|
47
|
-
? "You've been invited to play"
|
|
48
|
-
: "Waiting for everyone to accept";
|
|
49
|
-
case "active":
|
|
50
|
-
return xara.isMyTurn() ? "Your turn" : `${nameOf(state, session.turn)}'s turn`;
|
|
51
|
-
case "completed": {
|
|
52
|
-
const outcome = session.outcome!;
|
|
53
|
-
if (outcome.leftBy) return `${nameOf(state, outcome.leftBy)} left the game`;
|
|
54
|
-
if (!outcome.winners.length) return "It's a draw";
|
|
55
|
-
return outcome.winners.includes(me.id) ? "You won!" : `${nameOf(state, outcome.winners[0])} won`;
|
|
56
|
-
}
|
|
57
|
-
case "declined":
|
|
58
|
-
return "The invite was declined";
|
|
59
|
-
default:
|
|
60
|
-
return "This game was called off";
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function render(state: State) {
|
|
65
|
-
const total = totalOf(state.moves);
|
|
66
|
-
const playable = xara.isMyTurn() && !busy;
|
|
67
|
-
|
|
68
|
-
playersEl.replaceChildren(
|
|
69
|
-
...state.players.map((player) => {
|
|
70
|
-
const item = document.createElement("li");
|
|
71
|
-
item.textContent = player.name;
|
|
72
|
-
item.classList.toggle("turn", state.session.status === "active" && player.id === state.session.turn);
|
|
73
|
-
return item;
|
|
74
|
-
})
|
|
75
|
-
);
|
|
76
|
-
statusEl.textContent = statusText(state);
|
|
77
|
-
totalEl.textContent = `${total} / ${TARGET}`;
|
|
78
|
-
keys.forEach((button, i) => (button.disabled = !playable || !canAdd(total, STEPS[i])));
|
|
79
|
-
|
|
80
|
-
const invited = state.session.status === "invited" && state.players.find((player) => player.you)?.status === "invited";
|
|
81
|
-
acceptEl.classList.toggle("shown", invited && !busy);
|
|
82
|
-
acceptEl.disabled = !invited || busy;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function run(action: () => Promise<unknown>) {
|
|
86
|
-
busy = true;
|
|
87
|
-
noteEl.textContent = "";
|
|
88
|
-
if (xara.state) render(xara.state);
|
|
89
|
-
try {
|
|
90
|
-
await action();
|
|
91
|
-
} catch (error) {
|
|
92
|
-
noteEl.textContent = error instanceof XaraGameError ? error.message : "Something went wrong";
|
|
93
|
-
} finally {
|
|
94
|
-
busy = false;
|
|
95
|
-
if (xara.state) render(xara.state);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function play(add: number) {
|
|
100
|
-
const state = xara.state;
|
|
101
|
-
if (!state || !xara.isMyTurn() || busy) return;
|
|
102
|
-
const total = totalOf(state.moves);
|
|
103
|
-
if (!canAdd(total, add)) return;
|
|
104
|
-
// Every player reads the same summary, so it can't say "you".
|
|
105
|
-
const outcome = isWon(total + add)
|
|
106
|
-
? { winners: [state.me.id], summary: `${nameOf(state, state.me.id)} reached ${TARGET}` }
|
|
107
|
-
: undefined;
|
|
108
|
-
return run(() => xara.move({ add }, { outcome }));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
acceptEl.addEventListener("click", () => run(() => xara.accept()));
|
|
112
|
-
|
|
113
|
-
xara.onChange(render);
|
|
114
23
|
xara.load().catch((error) => {
|
|
115
24
|
statusEl.textContent = error instanceof XaraGameError ? error.message : "Couldn't start the game";
|
|
116
25
|
});
|
package/template/src/style.css
CHANGED
|
@@ -1,134 +1,18 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
|
|
3
|
-
--blue-soft: #e8efff;
|
|
4
|
-
--ink: #102a24;
|
|
5
|
-
--muted: #607080;
|
|
6
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
7
|
-
color: var(--ink);
|
|
2
|
+
color: #102a24;
|
|
8
3
|
background: #f4f8ff;
|
|
9
|
-
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
* {
|
|
13
|
-
box-sizing: border-box;
|
|
4
|
+
font-family: system-ui, sans-serif;
|
|
14
5
|
}
|
|
15
6
|
|
|
16
7
|
html,
|
|
17
8
|
body {
|
|
18
9
|
margin: 0;
|
|
19
10
|
height: 100%;
|
|
20
|
-
overscroll-behavior: none;
|
|
21
|
-
user-select: none;
|
|
22
|
-
-webkit-user-select: none;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/* Every row has a fixed height, so a new state never moves the page. */
|
|
26
|
-
main {
|
|
27
|
-
min-height: 100dvh;
|
|
28
|
-
max-width: 420px;
|
|
29
|
-
margin: 0 auto;
|
|
30
|
-
padding: max(24px, env(safe-area-inset-top)) 20px max(24px, env(safe-area-inset-bottom));
|
|
31
|
-
display: grid;
|
|
32
|
-
grid-template-rows: 40px 28px 96px 64px 52px 20px;
|
|
33
|
-
align-content: center;
|
|
34
|
-
row-gap: 20px;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.players {
|
|
38
|
-
margin: 0;
|
|
39
|
-
padding: 0;
|
|
40
|
-
list-style: none;
|
|
41
|
-
display: flex;
|
|
42
|
-
justify-content: center;
|
|
43
|
-
gap: 8px;
|
|
44
|
-
overflow: hidden;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.players li {
|
|
48
|
-
padding: 8px 12px;
|
|
49
|
-
border-radius: 14px;
|
|
50
|
-
background: #fff;
|
|
51
|
-
border: 1.5px solid transparent;
|
|
52
|
-
font-size: 14px;
|
|
53
|
-
font-weight: 600;
|
|
54
|
-
white-space: nowrap;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.players li.turn {
|
|
58
|
-
border-color: var(--blue);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.status,
|
|
62
|
-
.note {
|
|
63
|
-
margin: 0;
|
|
64
|
-
overflow: hidden;
|
|
65
|
-
text-overflow: ellipsis;
|
|
66
|
-
white-space: nowrap;
|
|
67
|
-
text-align: center;
|
|
68
11
|
}
|
|
69
12
|
|
|
70
13
|
.status {
|
|
71
|
-
line-height: 28px;
|
|
72
|
-
font-size: 18px;
|
|
73
|
-
font-weight: 700;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.total {
|
|
77
14
|
margin: 0;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
background: #fff;
|
|
82
|
-
font-size: 40px;
|
|
83
|
-
font-weight: 800;
|
|
84
|
-
font-variant-numeric: tabular-nums;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.pad {
|
|
88
|
-
display: grid;
|
|
89
|
-
grid-template-columns: repeat(3, 1fr);
|
|
90
|
-
gap: 10px;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.key {
|
|
94
|
-
border: 0;
|
|
95
|
-
border-radius: 18px;
|
|
96
|
-
background: var(--blue);
|
|
97
|
-
color: #fff;
|
|
98
|
-
font: inherit;
|
|
99
|
-
font-size: 22px;
|
|
100
|
-
font-weight: 800;
|
|
101
|
-
touch-action: manipulation;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.key:disabled {
|
|
105
|
-
background: var(--blue-soft);
|
|
106
|
-
color: var(--muted);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.actions {
|
|
110
|
-
display: grid;
|
|
111
|
-
place-items: center;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.action {
|
|
115
|
-
height: 48px;
|
|
116
|
-
padding: 0 24px;
|
|
117
|
-
border: 0;
|
|
118
|
-
border-radius: 999px;
|
|
119
|
-
background: var(--blue);
|
|
120
|
-
color: #fff;
|
|
121
|
-
font: inherit;
|
|
122
|
-
font-size: 15px;
|
|
123
|
-
font-weight: 700;
|
|
124
|
-
visibility: hidden;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.action.shown {
|
|
128
|
-
visibility: visible;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.note {
|
|
132
|
-
font-size: 13px;
|
|
133
|
-
color: #c0392b;
|
|
15
|
+
padding: 24px;
|
|
16
|
+
text-align: center;
|
|
17
|
+
font-size: 14px;
|
|
134
18
|
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import type { PlayedMove } from "@xavatech/xara-game-sdk";
|
|
3
|
-
import { canAdd, isWon, totalOf, type Move } from "./rules";
|
|
4
|
-
|
|
5
|
-
const moves = (adds: number[]): PlayedMove<Move>[] =>
|
|
6
|
-
adds.map((add, i) => ({ step: i + 1, playerId: (i % 2) + 1, move: { add }, at: "" }));
|
|
7
|
-
|
|
8
|
-
describe("rules", () => {
|
|
9
|
-
test("adds up every move, as after a reload", () => {
|
|
10
|
-
expect(totalOf(moves([3, 2, 1]))).toBe(6);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test("skips moves that break the rules", () => {
|
|
14
|
-
expect(totalOf(moves([3, 5, 3, 3, 3]))).toBe(9);
|
|
15
|
-
expect(canAdd(9, 2)).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("making the total 10 wins", () => {
|
|
19
|
-
expect(isWon(totalOf(moves([3, 3, 3, 1])))).toBe(true);
|
|
20
|
-
expect(isWon(9)).toBe(false);
|
|
21
|
-
});
|
|
22
|
-
});
|
package/template/src/rules.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { PlayedMove } from "@xavatech/xara-game-sdk";
|
|
2
|
-
|
|
3
|
-
/** A starter game: take turns adding 1, 2, or 3. Whoever makes the total 10 wins. Replace it with yours. */
|
|
4
|
-
export const TARGET = 10;
|
|
5
|
-
export const STEPS = [1, 2, 3] as const;
|
|
6
|
-
|
|
7
|
-
export type Move = { add: number };
|
|
8
|
-
|
|
9
|
-
/** The total after every move so far. Moves that don't follow the rules are skipped, on every phone alike. */
|
|
10
|
-
export function totalOf(moves: PlayedMove<Move>[]): number {
|
|
11
|
-
return moves.reduce((total, played) => (canAdd(total, played.move.add) ? total + played.move.add : total), 0);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function canAdd(total: number, add: number): boolean {
|
|
15
|
-
return (STEPS as readonly number[]).includes(add) && total + add <= TARGET;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function isWon(total: number): boolean {
|
|
19
|
-
return total === TARGET;
|
|
20
|
-
}
|