@netforge/template-cards 0.1.0 → 0.2.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/AGENTS.md ADDED
@@ -0,0 +1,71 @@
1
+ # Extending INKWRIT
2
+
3
+ INKWRIT is the card battler template of the Net Forge game engine. The template already has the duel, turns and the turn timer, the server-side shuffle, hidden hands, guards, rush, the draft, the AI, netcode, rendering, touch controls, earn and the store. You change the game by changing declarations. Read `llms.txt` for every field, and `llms-full.txt` for the complete worked example.
4
+
5
+ ## The one file to edit
6
+
7
+ - `src/game.ts` is the whole game. Change declarations in place.
8
+ - `src/names.json`, next to it, holds the title and every label players read. Cards go in `"weapons"` (`{ name, line }`), plus upgrades, earn rules and store items.
9
+ - Everything else (`src/manifest.ts`, `src/store.ts`, `client/`, `test/`, `scripts/`) is template machinery. Do not edit it.
10
+
11
+ ## Imports
12
+
13
+ `src/game.ts` imports only from `@netforge/engine`. The shipped game uses:
14
+
15
+ ```ts
16
+ import { card, cardTable, earn, game, store, upgrade } from "@netforge/engine";
17
+ ```
18
+
19
+ ## Declarations
20
+
21
+ - `game({ template: "cards", style, places, players: { prefab, max } })`, with `players.max` at most 2.
22
+ - `cardTable({ health, startHand, handMax, boardMax, manaMax, turnSeconds, draftSeconds, duels, starter, ai })`, exactly one. `ai` is 1 to 12 tiers of `{ health, smart }`; duel n against the AI uses tier n.
23
+ - `card(id, { model, kind: "unit" | "spell", cost, attack, health, guard?, rush?, effect? })`, 4 to 40 of them. Effects have kind `"damage"`, `"heal"`, `"draw"` or `"buff"` and target `"enemy"`, `"enemy_hero"`, `"all_enemies"`, `"own_hero"`, `"own_unit"` or `"all_own"`.
24
+ - `upgrade(id, { stat, step, maxLevel })` with stat `maxHealth`, `might`, `haste` or `amount` only.
25
+ - `earn.rule(id, { when, n, pay, oncePer: "run" })`. Events: `duel.won` (against a person, never a forfeit), `ai.beaten` and `run.won`.
26
+ - `earn.stake({ min, max, multiple })`, exactly one, on one top-level line.
27
+ - `store.sku(id, { coins, grants? })`.
28
+
29
+ ## Rules the build enforces
30
+
31
+ - Ids are lower_snake_case.
32
+ - Every game declares at least one `earn.rule` and exactly one `earn.stake`. `"stake"` is reserved.
33
+ - The starter deck has 8 to 30 cards, names declared cards, and holds at least one unit and cards costing 2 or less.
34
+ - A unit has health 1 or more. A spell has an effect.
35
+ - No `weapon(...)` in a card game.
36
+ - Asset ids are library ids such as `"style.lowpoly.frog"`, never URLs or paths. Keep `game({ style })` as the creator picked it.
37
+ - Two playtest players must decide duels and fire an earn rule within 4 minutes.
38
+ - Keep the SKUs the client is wired to at their prices: `thick_vellum`, `sharp_nib`, `quick_ink`, `spare_page`, `skin_vermilion`, `skin_gold_leaf` and `bound_folio`.
39
+ - Nothing in `src/game.ts` can reveal deck order or another player's hand.
40
+
41
+ ## Validate
42
+
43
+ - With the MCP server (`npx -y @netforge/mcp`), call `netforge_validate_game` on `src/game.ts`. It returns one actionable line per problem.
44
+ - In this package, run the tests with vitest: `test/harness.test.ts` and `test/store.test.ts`.
45
+
46
+ ## Playtest
47
+
48
+ - With the MCP server, call `netforge_run_playtest`.
49
+ - In code, import the game module and run the headless harness:
50
+
51
+ ```ts
52
+ import { engine } from "@netforge/engine";
53
+ import "./src/game.js";
54
+
55
+ const report = await engine.test.run({ ticks: 30 * 240, bots: 2, seed: 1 });
56
+ console.log(report.kills, report.deaths, report.earned, report.rules?.checks);
57
+ ```
58
+
59
+ `bots: 2` plays two people; `bots: 1` plays one person against the AI. The report has duels won (kills) and lost (deaths), earn by rule, softlock, ms per tick and the genre's checks under `rules.checks`.
60
+
61
+ ## Play locally
62
+
63
+ In a folder with `netnet.json` (`{"template":"cards"}`) and `src/game.ts`, with `@netforge/kit-play` and `@netforge/engine` installed, run `npx kit-play`. It opens http://127.0.0.1:4663 with play money. The MCP tool `netforge_play_locally` gives the same steps.
64
+
65
+ ## Do not
66
+
67
+ - Do not write renderers, netcode, physics or AI. The template has them.
68
+ - Do not reference assets by URL or file path.
69
+ - Do not import anything other than `@netforge/engine` in `src/game.ts`.
70
+ - Do not edit `client/`, `src/manifest.ts` or `src/store.ts`.
71
+ - Do not rename or reprice the SKUs the client is wired to.
package/CLAUDE.md ADDED
@@ -0,0 +1,11 @@
1
+ # CLAUDE.md
2
+
3
+ This package is INKWRIT, a Net Forge game template. Read AGENTS.md before changing this game. It lists the declarations this genre has, the rules the build enforces, and how to validate, playtest and play the game locally.
4
+
5
+ The five hard rules:
6
+
7
+ 1. Edit only src/game.ts (and src/names.json for the labels players read).
8
+ 2. Import only from @netforge/engine.
9
+ 3. Ids are lower_snake_case.
10
+ 4. Declare at least one earn.rule and exactly one earn.stake.
11
+ 5. Never write renderers, netcode, physics or bot AI, and never reference assets by URL or file path (use library ids such as "style.lowpoly.frog").
package/README.md CHANGED
@@ -1,47 +1,83 @@
1
- # INKWRIT (card battler template)
1
+ # @netforge/template-cards
2
2
 
3
- Scribes duel in a lamplit study. Every card is an inked creature or a
4
- spell written on a page. One against one, a person against a person or
5
- against the gauntlet's AI scribes; a run is four duels with a draft of one
6
- of three cards after each.
3
+ A card battler game template for Net Forge: one against one, a person against a person or against a gauntlet of AI scribes (INKWRIT).
7
4
 
8
- A board genre (`packages/engine/src/rules/cards.ts`) on the room server's
9
- SimAdapter seam, drawn with `@netforge/tabletop`.
5
+ Scribes duel in a lamplit study. Every card is an inked creature or a spell written on a page. A run is four duels, and after each one every player drafts one of three new cards into their deck. The room shuffles on the server and keeps hands hidden.
10
6
 
11
- | Path | What |
12
- |---|---|
13
- | `src/game.ts` | The game, as the model writes it (declarations only). |
14
- | `src/manifest.ts` | Loads it; `cardsAdapter()` for the room server and client. Room template `cards` (two seats). |
15
- | `src/store.ts` | Coin prices the client is wired to, the template's own coin (INK). |
16
- | `client/` | The desk, creatures on their cards, piles, quills, the hand, targeting, the draft. |
17
- | `test/` | Playtest bots and the store. |
18
- | `llms.txt` | How to change the game (for agents). |
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @netforge/template-cards
11
+ ```
12
+
13
+ You rarely import this package directly. kit play (`@netforge/kit-play`) and the Net Forge MCP server use it to build and run a game whose `src/game.ts` you write.
14
+
15
+ ## Play it locally
16
+
17
+ ```sh
18
+ mkdir -p my-game/src && cd my-game
19
+ npm init -y && npm install @netforge/kit-play @netforge/engine @netforge/template-cards
20
+ cp node_modules/@netforge/template-cards/src/game.ts src/
21
+ echo '{"template":"cards"}' > netnet.json
22
+ npx kit-play # opens http://127.0.0.1:4663
23
+ ```
24
+
25
+ Open the link in a second tab to duel yourself, or play alone against the AI. Coins are play money. Real-token earning, stakes and claims run only on the hosted Net Forge server.
26
+
27
+ ## Change the game
28
+
29
+ Edit `src/game.ts` and run `npx kit-play` again. For example, give the heroes more health and add a fifth duel to the gauntlet:
30
+
31
+ ```ts
32
+ cardTable({
33
+ health: 25,
34
+ duels: 5,
35
+ // ...the rest of the table as shipped, with a fifth ai tier
36
+ });
37
+ ```
38
+
39
+ Or add a card and put its label in `src/names.json` under `weapons`:
40
+
41
+ ```ts
42
+ card("mirror_moth", { model: "style.lowpoly.jelly_flyer", kind: "unit", cost: 3, attack: 2, health: 2, effect: { kind: "damage", amount: 1, target: "all_enemies" } });
43
+ ```
19
44
 
20
45
  ## How it plays on the server
21
46
 
22
- - Server-authoritative at 30 Hz. Your quill (the cursor) is predicted; a
23
- play, an attack, an end of turn and a draft pick are actions the room
24
- validates (whose turn, the card in that slot, ink, guards).
25
- - Hidden information: the room shuffles from its seeded PRNG and keeps
26
- the deck order to itself; a hand and a draft offer go only to their
27
- owner (`SimAdapter.infoFor`). Snapshots carry the quills only.
28
- The room server's tests check it every few ticks of a run.
29
- - A person alone for 3 s gets a seat bot (an AI scribe of the gauntlet's
30
- tier); it leaves when a second person joins (that duel is called off).
31
- - Earn (the room is the referee): duels won against a person (never a
32
- forfeit), AI scribes beaten. The AI never earns. Pots stay off.
47
+ - A board genre on the engine's rules runtime (`@netforge/engine/rules`), drawn with `@netforge/tabletop`, at 30 Hz.
48
+ - Your quill (the cursor) is predicted. A play, an attack, an end of turn and a draft pick are actions the room validates: whose turn it is, the card in that slot, ink and guards.
49
+ - Hidden information: the room shuffles from its seeded PRNG and keeps the deck order to itself. A hand and a draft offer go only to their owner.
50
+ - A person alone for 3 s gets a seat bot (an AI scribe of the gauntlet's tier), which leaves when a second person joins.
51
+ - Earn: duels won against a person (never a forfeit) and AI scribes beaten. The AI never earns. The room server is the referee.
33
52
 
34
- ## Run it
53
+ ## Files
54
+
55
+ | Path | What |
56
+ |---|---|
57
+ | `src/game.ts` | The game, as declarations only. This is the file you edit. |
58
+ | `src/manifest.ts` | Loads the game; `cardsAdapter()` for the room host and the client (two seats). |
59
+ | `src/store.ts` | Coin prices the client is wired to. |
60
+ | `client/` | The desk, creatures on their cards, piles, quills, the hand, targeting and the draft. |
61
+ | `test/` | Playtest bots through `engine.test.run`, and the store test. |
62
+ | `llms.txt`, `llms-full.txt` | How to change the game, for agents. |
35
63
 
36
- This template runs on your own computer with play money through
37
- `kit play` (`@netforge/kit-play`). From the repository root:
64
+ ## For AI agents
65
+
66
+ Read `AGENTS.md` and `llms.txt` before editing. The Net Forge MCP server gives a coding agent the templates, the API and a playtest loop:
38
67
 
39
68
  ```sh
40
- pnpm install --frozen-lockfile && pnpm build
41
- mkdir -p play-cards/src && cp packages/template-cards/src/game.ts play-cards/src/
42
- echo '{"template":"cards"}' > play-cards/netnet.json
43
- node packages/kit-play/dist/cli.js play-cards # opens http://127.0.0.1:4663
69
+ npx -y @netforge/mcp
44
70
  ```
45
71
 
46
- Edit `play-cards/src/game.ts` and run it again to try a change. Real-token
47
- earning, stakes and claims run only on the hosted Net Forge server.
72
+ The package is `@netforge/mcp` and its bin is `netforge-mcp`. Its tools are `netforge_list_templates`, `netforge_create_game`, `netforge_explain_api`, `netforge_search_docs`, `netforge_validate_game`, `netforge_run_playtest` and `netforge_play_locally`.
73
+
74
+ To set up a coding agent in a game project, run `npx netforge setup agent`. It writes AGENTS.md, CLAUDE.md, Cursor rules and `.mcp.json` into the project. In Claude Code you can also add the server directly with `claude mcp add netforge -- npx -y @netforge/mcp`.
75
+
76
+ ## Docs
77
+
78
+ - https://forge.netnet.capital/docs/templates/cards
79
+ - `llms.txt` and `llms-full.txt` in this package. The full file includes the complete `src/game.ts` and the validate and playtest steps.
80
+
81
+ ## License
82
+
83
+ MIT. See LICENSE and NOTICE.
package/llms-full.txt ADDED
@@ -0,0 +1,243 @@
1
+ # Net Forge card battler template (INKWRIT)
2
+
3
+ > A one-against-one card battler. Two scribes duel at a desk: heroes with
4
+ > health, ink (mana) that rises each turn, a hand drawn from a shuffled
5
+ > deck, inked creatures on the board, spells. A run is a gauntlet of
6
+ > duels; after each one, every player drafts one of three cards into
7
+ > their deck. Alone, an AI scribe of the gauntlet takes the other seat.
8
+ > You change the game by editing src/game.ts only. The template already has
9
+ > the duel, turns and the turn timer, the shuffle (on the server), hidden
10
+ > hands, guards, rush, the draft, the AI, netcode, rendering, touch,
11
+ > earn and the store. Do not write renderers, netcode or AI.
12
+
13
+ ## Files
14
+
15
+ - src/game.ts: THE GAME. Declarations against @netforge/engine:
16
+ game(), cardTable(), card(), upgrade(), earn.rule(), earn.stake(), store.sku().
17
+ - src/names.json: the title and every label players read. Cards go in
18
+ "weapons" ({ name, line }), plus upgrades, earn rules and store items.
19
+ - Everything else is template machinery.
20
+
21
+ ## Rules the build enforces
22
+
23
+ - game({ template: "cards", style, places, players: { prefab, max } }), players.max at most 2.
24
+ - Exactly one cardTable(...); 4 to 40 card(...)s. The starter deck (8 to 30
25
+ cards) names declared cards, holds at least one unit and cards costing 2
26
+ or less. ai is 1 to 12 tiers { health, smart }.
27
+ - A unit has health 1 or more; a spell has an effect.
28
+ - Effects: kind "damage", "heal", "draw" or "buff"; target "enemy" (the
29
+ player picks an enemy unit or the hero), "enemy_hero", "all_enemies",
30
+ "own_hero", "own_unit" (the player picks one of theirs) or "all_own".
31
+ - Upgrades read four stats only: maxHealth (hero health), might (+attack on
32
+ the buyer's units), haste (+ink in the first turn), amount (+cards in
33
+ the first hand).
34
+ - No weapon(...). Asset ids look like "style.lowpoly.frog"; keep game({ style }).
35
+ - 2 playtest players must decide duels and fire an earn rule within 4 minutes.
36
+
37
+ ## Units
38
+
39
+ Health and attack in points, cost in ink (0 to 10), seconds for timers.
40
+ Tick rate 30 Hz.
41
+
42
+ ## The loop
43
+
44
+ A duel: a coin decides who goes first; the second player draws one extra
45
+ card. Each turn: +1 ink (up to manaMax), draw a card (an empty deck hurts
46
+ more each time), play cards, attack with ready units (a unit is ready the
47
+ turn after it lands, or at once with rush; guard units must be attacked
48
+ first), end the turn (or the timer ends it). A hero at 0 loses; turnCap
49
+ turns is a draw. After each duel but the last, a draft: one of three.
50
+
51
+ ## Common changes
52
+
53
+ ### A new card
54
+ card("mirror_moth", { model: "style.lowpoly.jelly_flyer", kind: "unit", cost: 3, attack: 2, health: 2, effect: { kind: "damage", amount: 1, target: "all_enemies" } });
55
+ Add its label to names.json weapons. The draft offers every card unless cardTable pool lists some.
56
+
57
+ ### Tune the table
58
+ cardTable({ health: 25, duels: 5, turnSeconds: 40, ... }). Lower health
59
+ makes faster duels; more duels make longer runs.
60
+
61
+ ### Harder or easier AI
62
+ ai: [{ health: 14, smart: 0.3 }, ...]: duel n against the AI uses tier n;
63
+ raise health and smart for a harder gauntlet.
64
+
65
+ ### Earn rules
66
+ earn.rule("ink_master", { when: "ai.beaten", n: 3, pay: 15, oncePer: "run" });
67
+ Events: duel.won (a duel won against a person; never against the AI, never
68
+ a forfeit), ai.beaten (a gauntlet AI scribe beaten; a forfeit pays
69
+ nothing), run.won (more duels won than lost in the run). Each rule pays
70
+ once per run per player, in coins; the room server is the referee; the AI
71
+ scribe never earns.
72
+
73
+ ### The buy-in run (required)
74
+ earn.stake({ min: 10, max: 100, multiple: 2 }); one top-level line. "stake" is reserved.
75
+
76
+ ### Coins: earn and the store
77
+ store.sku("thick_vellum", { coins: 40 }); // one level of the upgrade for the run
78
+ store.sku("skin_gold_leaf", { coins: 500, grants: { cosmetic: 2 } }); // a finish for your quill, owned for good
79
+ store.sku("bound_folio", { coins: 120, grants: { gear: "thick_vellum", loseBelow: 0.25 } }); // losable gear
80
+ Keep the SKUs the client is wired to (thick_vellum, sharp_nib, quick_ink,
81
+ spare_page, skin_vermilion, skin_gold_leaf, bound_folio) at their prices.
82
+
83
+ ## Hidden information
84
+
85
+ Decks are shuffled on the room server and their order never leaves it; a
86
+ hand and a draft offer are sent only to their owner. Nothing in game.ts
87
+ can change that.
88
+
89
+ ## Test
90
+
91
+ engine.test.run({ ticks: 30 * 240, bots: 2 }) plays duels between two
92
+ players (bots: 1 plays one person against the AI) and reports duels won
93
+ (kills) and lost (deaths), the duel reached (wave), runs, earned by rule,
94
+ softlock, ms per tick and the genre's checks under rules.checks.
95
+
96
+ ## Worked example: src/game.ts
97
+
98
+ This is the template's complete src/game.ts as it ships. A new game on this
99
+ template starts from this file and changes declarations in place.
100
+
101
+ ```ts
102
+ /**
103
+ * INKWRIT: the card battler template (play style:
104
+ * card battler).
105
+ *
106
+ * Scribes duel in a lamplit study. Every card is an inked creature or a
107
+ * spell written on a page: blot imps, ledger crabs, folio wyrms, a splash
108
+ * of ink. One against one, a person against a person or against the
109
+ * gauntlet's AI scribes. A run is four duels; after each, you draft one of
110
+ * three new cards into your deck.
111
+ *
112
+ * This file is the whole game as the model writes it: declarations only,
113
+ * against the typed API in @netforge/engine. The duel, the draft, the
114
+ * shuffle, hidden hands, the AI, netcode, renderer and referee are the
115
+ * template's machinery.
116
+ */
117
+ import { card, cardTable, earn, game, store, upgrade } from "@netforge/engine";
118
+
119
+ export default game({
120
+ template: "cards",
121
+ style: "lowpoly",
122
+ places: { study: { chunks: "places/study", spawn: [0, 0, 0] } },
123
+ players: { prefab: "style.lowpoly.hooded_adventurer", max: 2 },
124
+ });
125
+
126
+ // The table: hero health, hands, the board, the gauntlet and its AI scribes (tier n plays duel n against the AI).
127
+ cardTable({
128
+ health: 20,
129
+ startHand: 3,
130
+ handMax: 7,
131
+ boardMax: 5,
132
+ manaMax: 10,
133
+ turnSeconds: 30,
134
+ draftSeconds: 15,
135
+ duels: 4,
136
+ starter: ["blot_imp", "blot_imp", "quill_sprite", "quill_sprite", "ink_frog", "ink_frog", "margin_bat", "ledger_crab", "serif_fox", "splash", "splash", "blotting_sand"],
137
+ ai: [
138
+ { health: 14, smart: 0.3 },
139
+ { health: 16, smart: 0.55 },
140
+ { health: 20, smart: 0.75 },
141
+ { health: 24, smart: 0.9 },
142
+ ],
143
+ });
144
+
145
+ // Units: cost in mana, attack and health. guard units must be attacked first; rush units attack the turn they land.
146
+ card("blot_imp", { model: "style.lowpoly.imp", kind: "unit", cost: 1, attack: 2, health: 1 });
147
+ card("quill_sprite", { model: "style.lowpoly.bee", kind: "unit", cost: 1, attack: 1, health: 2, rush: true });
148
+ card("ink_frog", { model: "style.lowpoly.frog", kind: "unit", cost: 2, attack: 2, health: 3 });
149
+ card("margin_bat", { model: "style.lowpoly.bat", kind: "unit", cost: 2, attack: 3, health: 1, rush: true });
150
+ card("ledger_crab", { model: "style.lowpoly.crab", kind: "unit", cost: 3, attack: 2, health: 5, guard: true });
151
+ card("serif_fox", { model: "style.lowpoly.fox", kind: "unit", cost: 3, attack: 3, health: 3, effect: { kind: "draw", amount: 1 } });
152
+ card("gloss_wolf", { model: "style.lowpoly.wolf", kind: "unit", cost: 4, attack: 4, health: 4 });
153
+ card("stamp_golem", { model: "style.lowpoly.yeti", kind: "unit", cost: 5, attack: 4, health: 7, guard: true });
154
+ card("folio_wyrm", { model: "style.lowpoly.dragon_whelp", kind: "unit", cost: 6, attack: 5, health: 5, effect: { kind: "damage", amount: 2, target: "all_enemies" } });
155
+ card("grand_errata", { model: "style.lowpoly.dragon", kind: "unit", cost: 8, attack: 8, health: 8 });
156
+ // Spells: an effect only. "enemy" lets the player pick an enemy unit or the hero.
157
+ card("splash", { model: "style.lowpoly.crystal", kind: "spell", cost: 1, effect: { kind: "damage", amount: 2, target: "enemy" } });
158
+ card("blotting_sand", { model: "style.lowpoly.gem_mid", kind: "spell", cost: 2, effect: { kind: "heal", amount: 5, target: "own_hero" } });
159
+ card("underline", { model: "style.lowpoly.gem_pink", kind: "spell", cost: 2, effect: { kind: "buff", amount: 2, target: "own_unit" } });
160
+ card("second_draft", { model: "style.lowpoly.chest", kind: "spell", cost: 3, effect: { kind: "draw", amount: 2 } });
161
+ card("ink_storm", { model: "style.lowpoly.meteor", kind: "spell", cost: 4, effect: { kind: "damage", amount: 2, target: "all_enemies" } });
162
+ card("final_word", { model: "style.lowpoly.crystal_rock", kind: "spell", cost: 5, effect: { kind: "damage", amount: 5, target: "enemy" } });
163
+
164
+ // Store upgrades: one level each, for the rest of the run (hero health, unit attack, first-turn mana, starting hand).
165
+ upgrade("thick_vellum", { stat: "maxHealth", step: 5, maxLevel: 1 });
166
+ upgrade("sharp_nib", { stat: "might", step: 1, maxLevel: 1 });
167
+ upgrade("quick_ink", { stat: "haste", step: 1, maxLevel: 1 });
168
+ upgrade("spare_page", { stat: "amount", step: 1, maxLevel: 1 });
169
+
170
+ // Earn (mandatory). Pays the game's coins (1 coin = 1 token), once per run each. Values are PLACEHOLDER.
171
+ // duel.won counts duels won against a person (never against the AI, never a forfeit); ai.beaten counts gauntlet AI scribes beaten.
172
+ earn.rule("first_ink", { when: "ai.beaten", n: 1, pay: 5, oncePer: "run" });
173
+ earn.rule("full_gauntlet", { when: "ai.beaten", n: 4, pay: 20, oncePer: "run" });
174
+ earn.rule("inked_duel", { when: "duel.won", n: 1, pay: 12, oncePer: "run" });
175
+ earn.rule("signed_and_sealed", { when: "duel.won", n: 3, pay: 25, oncePer: "run" });
176
+ // Buy-in run (the mandatory sink loop): stake coins at run start to earn above free play. Values are PLACEHOLDER.
177
+ earn.stake({ min: 10, max: 100, multiple: 2 });
178
+
179
+ // Store (the Kit), priced in coins: four run upgrades and two finishes for your quill, owned for good.
180
+ store.sku("thick_vellum", { coins: 40 });
181
+ store.sku("sharp_nib", { coins: 60 });
182
+ store.sku("quick_ink", { coins: 40 });
183
+ store.sku("spare_page", { coins: 50 });
184
+ store.sku("skin_vermilion", { coins: 300, grants: { cosmetic: 1 } });
185
+ store.sku("skin_gold_leaf", { coins: 500, grants: { cosmetic: 2 } });
186
+ // Losable gear: one level of its upgrade in every staked run, lost when that run makes under loseBelow of its most earn. PLACEHOLDER.
187
+ store.sku("bound_folio", { coins: 120, grants: { gear: "thick_vellum", loseBelow: 0.25 } });
188
+ ```
189
+
190
+ ## Validate and playtest
191
+
192
+ ### With the MCP server
193
+
194
+ Run `npx -y @netforge/mcp` (package @netforge/mcp, bin netforge-mcp) and
195
+ connect it to your coding agent. Its tools are netforge_list_templates, netforge_create_game,
196
+ netforge_explain_api, netforge_search_docs, netforge_validate_game, netforge_run_playtest and netforge_play_locally.
197
+
198
+ - netforge_validate_game checks src/game.ts against the engine's schemas and this
199
+ template's rules, and returns one actionable line per problem.
200
+ - netforge_run_playtest plays the game headless with bots and returns the report.
201
+ - netforge_play_locally gives the steps to open the game in a browser with kit play.
202
+
203
+ ### In code
204
+
205
+ Import the game module first so its declarations register, then run the
206
+ headless harness. This plays two players for 4 minutes at 30 Hz (bots: 1 plays one person against the AI).
207
+
208
+ ```ts
209
+ import { engine } from "@netforge/engine";
210
+ import "./src/game.js";
211
+
212
+ const report = await engine.test.run({
213
+ ticks: 30 * 240,
214
+ bots: 2,
215
+ seed: 1,
216
+ assert: [
217
+ { name: "earn fired", check: (r) => r.earnEmitted > 0 },
218
+ { name: "no softlock", check: (r) => !r.softlock },
219
+ ],
220
+ });
221
+ if (report.failed.length > 0) throw new Error(report.failed.join("\n"));
222
+ ```
223
+
224
+ The report has duels won (kills) and lost (deaths), the duel reached (wave), runs, earned by rule, softlock, ms per tick and the genre's checks under rules.checks.
225
+
226
+ The template's own tests (vitest) are the reference checks:
227
+
228
+ - test/harness.test.ts
229
+ - test/store.test.ts
230
+
231
+ ### Play it locally
232
+
233
+ In a folder with netnet.json and src/game.ts:
234
+
235
+ ```sh
236
+ npm install @netforge/kit-play @netforge/engine @netforge/template-cards
237
+ echo '{"template":"cards"}' > netnet.json
238
+ npx kit-play # opens http://127.0.0.1:4663
239
+ ```
240
+
241
+ kit play builds the template client with your src/game.ts and runs a local
242
+ room host with play money. No wallet, no chain and no hosted servers are
243
+ involved. Open the link in more tabs for more players.
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # INKWRIT: the card battler template (NetNet game runtime, board genre)
1
+ # Net Forge card battler template (INKWRIT)
2
2
 
3
3
  > A one-against-one card battler. Two scribes duel at a desk: heroes with
4
4
  > health, ink (mana) that rises each turn, a hand drawn from a shuffled
package/package.json CHANGED
@@ -1,12 +1,34 @@
1
1
  {
2
2
  "name": "@netforge/template-cards",
3
- "version": "0.1.0",
4
- "description": "INKWRIT: the card battler template of the NetNet game runtime, one against one or against the AI, a gauntlet of duels with a draft between them, server-side shuffle and hidden hands. The game module, its browser client and its playtest bots.",
3
+ "version": "0.2.0",
4
+ "description": "Card battler game template for Net Forge: one against one or against the AI, a gauntlet of duels with drafts, server-side shuffle and hidden hands (INKWRIT).",
5
+ "keywords": [
6
+ "game-template",
7
+ "multiplayer",
8
+ "browser-game",
9
+ "html5-game",
10
+ "threejs",
11
+ "game-engine",
12
+ "typescript",
13
+ "gamedev",
14
+ "ai-agents",
15
+ "mcp",
16
+ "net-forge",
17
+ "card-game"
18
+ ],
5
19
  "license": "MIT",
6
20
  "type": "module",
21
+ "sideEffects": [
22
+ "./src/game.ts",
23
+ "./src/manifest.ts",
24
+ "./client/**"
25
+ ],
7
26
  "exports": {
8
27
  ".": "./src/manifest.ts",
9
- "./store": "./src/store.ts"
28
+ "./store": "./src/store.ts",
29
+ "./package.json": "./package.json",
30
+ "./llms.txt": "./llms.txt",
31
+ "./llms-full.txt": "./llms-full.txt"
10
32
  },
11
33
  "files": [
12
34
  "src",
@@ -15,26 +37,32 @@
15
37
  "llms.txt",
16
38
  "README.md",
17
39
  "NOTICE",
18
- "LICENSE"
40
+ "LICENSE",
41
+ "llms-full.txt",
42
+ "AGENTS.md",
43
+ "CLAUDE.md"
19
44
  ],
20
45
  "publishConfig": {
21
46
  "access": "public"
22
47
  },
48
+ "homepage": "https://forge.netnet.capital/docs/templates/cards",
49
+ "bugs": {
50
+ "url": "https://github.com/netnetdev/net-forge/issues"
51
+ },
23
52
  "repository": {
24
53
  "type": "git",
25
54
  "url": "git+https://github.com/netnetdev/net-forge.git",
26
55
  "directory": "packages/template-cards"
27
56
  },
28
- "homepage": "https://github.com/netnetdev/net-forge/tree/main/packages/template-cards#readme",
29
57
  "engines": {
30
58
  "node": ">=20"
31
59
  },
32
60
  "dependencies": {
33
61
  "three": "0.186.1",
34
- "@netforge/asset-library": "0.1.0",
35
- "@netforge/tabletop": "0.1.0",
36
- "@netforge/engine": "0.1.0",
37
- "@netforge/engine-sim": "0.1.0"
62
+ "@netforge/asset-library": "0.2.0",
63
+ "@netforge/engine": "0.2.0",
64
+ "@netforge/engine-sim": "0.2.0",
65
+ "@netforge/tabletop": "0.2.0"
38
66
  },
39
67
  "devDependencies": {
40
68
  "@types/node": "22.20.1",
@@ -44,9 +72,6 @@
44
72
  "vite": "6.4.3",
45
73
  "vitest": "3.2.7"
46
74
  },
47
- "bugs": {
48
- "url": "https://github.com/netnetdev/net-forge/issues"
49
- },
50
75
  "scripts": {
51
76
  "build": "node ../engine/scripts/fresh-dist.mjs && vite build --config client/vite.config.ts",
52
77
  "typecheck": "tsc -p tsconfig.json",