@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 +71 -0
- package/CLAUDE.md +11 -0
- package/README.md +71 -35
- package/llms-full.txt +243 -0
- package/llms.txt +1 -1
- package/package.json +37 -12
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
|
-
#
|
|
1
|
+
# @netforge/template-cards
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netforge/template-cards",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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.
|
|
35
|
-
"@netforge/
|
|
36
|
-
"@netforge/engine": "0.
|
|
37
|
-
"@netforge/
|
|
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",
|