Card battler template with AI opponents (INKWRIT)
Net Forge, the open-source multiplayer browser game engine built for AI coding agents, is published on npm under @netforge with source at github.com/netnetdev/net-forge.
INKWRIT is a complete online card battler for two players: each hero has health, ink (mana) rises each turn, hands are drawn from decks the server shuffles and keeps secret, and units and spells fight on a shared desk. A run is a gauntlet of four duels with a draft of one of three cards after each, and a player alone faces AI opponents of rising strength.
npm create netforge@latest my-game -- --template cards --yes
cd my-game
npm run playOpen the link in one tab to play the AI gauntlet. Open it in a second tab and the two of you duel each other.
What you get
| Part | What it does | Where |
|---|---|---|
| The table | Hero health, starting hand, hand and board limits, mana cap, turn and draft timers, duels per run, the starter deck | cardTable({...}) |
| Cards | 4 to 40 cards: units (attack, health, guard, rush) and spells, each with an optional effect | card(...) |
| Effects | damage, heal, draw, buff on targets such as one enemy, all enemies, your hero or one of your units | card({ effect }) |
| AI opponents | 1 to 12 tiers of health and play strength; a seat bot joins a player who is alone | cardTable({ ai }) |
| Hidden information | Deck order never leaves the server; each hand and draft offer goes only to its owner | the engine |
| Netcode | 30 Hz authoritative room; plays, attacks, end of turn and draft picks are validated actions | the template package |
| Playtest bots | Two players for four minutes: duels decided, earn | npm test |
Measured on an Apple M2 Pro, Node 22: two bots over four minutes (7,200 ticks at 30 Hz) played into the fourth duel of the gauntlet and completed three runs, at 0.002 ms per server tick on average.
1. Add a card
import { card } from "@netforge/engine";
card("mirror_moth", {
model: "style.lowpoly.jelly_flyer",
kind: "unit",
cost: 3,
attack: 2,
health: 2,
effect: { kind: "damage", amount: 1, target: "all_enemies" },
});
card("final_word", { model: "style.lowpoly.crystal_rock", kind: "spell", cost: 5, effect: { kind: "damage", amount: 5, target: "enemy" } });A unit needs health of 1 or more and a spell needs an effect. Add a label in src/names.json. The draft offers every card unless cardTable({ pool }) lists a subset.
2. Tune the table and the AI
import { cardTable } from "@netforge/engine";
cardTable({
health: 25,
startHand: 3,
handMax: 7,
boardMax: 5,
manaMax: 10,
turnSeconds: 40,
draftSeconds: 15,
duels: 5,
starter: ["blot_imp", "blot_imp", "quill_sprite", "quill_sprite", "ink_frog", "ink_frog", "margin_bat", "ledger_crab", "serif_fox", "splash", "splash", "blotting_sand"],
ai: [
{ health: 14, smart: 0.3 },
{ health: 16, smart: 0.55 },
{ health: 20, smart: 0.75 },
{ health: 24, smart: 0.9 },
{ health: 28, smart: 1 },
],
});Duel n against the AI uses tier n. Lower hero health makes faster duels; more duels make longer runs. The starter deck holds 8 to 30 declared cards, with at least one unit and some cards costing 2 or less.
3. Add an earn rule
import { earn } from "@netforge/engine";
earn.rule("ink_master", { when: "ai.beaten", n: 3, pay: 15, oncePer: "run" });duel.won counts duels won against a person (never against the AI, never a forfeit), ai.beaten counts AI opponents beaten, and run.won counts runs with more duels won than lost. The AI never earns.
4. Check it with bots
npm testHow it works
A duel is a board genre: a deterministic rules module in @netforge/engine/rules with integer state and a seeded random number on the room server. The server shuffles, keeps the deck order to itself, and sends each player only their own hand and draft offer; snapshots carry the cursors only. The hosted room server is tested for hidden-card leaks every few ticks of a run, and nothing in src/game.ts can change that. Turns, the turn timer, guards and rush are part of the genre, so your file only declares the cards and the table.
The full guide is Build an online card game with turns and hidden information.
Alternatives
- boardgame.io is an open-source JavaScript engine for turn-based games, with moves, phases, secret state, a lobby and multiplayer built in. Pick it when you want to describe any turn-based rules in your own code and draw them with your own UI.
- Colyseus is a multiplayer server framework with rooms and state sync. Pick it when you have a card client already and want a server to hold the game state.
- Net Forge fits when you want a finished card battler with a 3D desk, server-side shuffles, hidden hands, AI opponents and a draft, and you want to change the cards rather than build the duel.
FAQ
How does the card game keep hands hidden?
The room server shuffles from its own seeded random number and keeps deck order to itself. A hand and a draft offer are sent only to their owner through a per-player channel, and snapshots carry only the players' cursors.
Can I play against the computer?
Yes. A player alone for 3 seconds gets an AI opponent from the gauntlet's tier for that duel. When a second person joins, the AI duel is called off and the two people play each other.
How many cards can a game have?
From 4 to 40 cards, with a starter deck of 8 to 30.
Is it turn-based with a timer?
Yes. Each turn has turnSeconds (30 in the template), and the timer ends the turn if the player does not. A duel that reaches the turn cap is a draw.
Can I make it a deckbuilder?
The draft between duels already adds one of three cards to your deck each time. You control which cards it offers with cardTable({ pool }).