# How to make an online card battler with AI opponents

> Start the INKWRIT card battler with one command: one-against-one duels with turns, a turn timer, hidden hands shuffled on the server, a draft between duels and AI opponents in four tiers. Change cards, the table and the AI in src/game.ts.

Net Forge, the open-source multiplayer browser game engine built for AI coding agents, is published on npm under `@netforge` with source at https://github.com/netnetdev/net-forge.

Source: https://forge.netnet.capital/docs/guides/online-card-game  
Last updated: 2026-10-23. Checked against @netforge/engine 0.2.0.

Run `npm create netforge@latest my-cards -- --template cards --yes` and you have an online card battler: two players duel with a deck, a hand, mana that rises each turn, creatures and spells, and when you play alone an AI opponent takes the other seat. Decks are shuffled on the server and a hand is only ever sent to its owner, so hidden information is handled for you.

```sh
npm create netforge@latest my-cards -- --template cards --yes
cd my-cards
npm run play
```

You change the game by declaring cards and the table in `src/game.ts`. The turn structure, the shuffle, targeting, guards and rush, the draft, the AI, the netcode and the 3D desk are the template's.

## What you get

| Part | Where | Notes |
|---|---|---|
| Turns and timer | `cardTable({ turnSeconds })` | A coin decides who goes first; the second player draws one extra card; the timer ends a slow turn. |
| Hidden information | room server | The deck order never leaves the server. A hand and a draft offer go only to their owner. |
| Cards | `card({...})` | 4 to 40 units and spells. Effects: `damage`, `heal`, `draw`, `buff`, with targets such as `enemy`, `all_enemies`, `own_unit`. |
| AI opponents | `cardTable({ ai })` | 1 to 12 tiers of `{ health, smart }`; duel n against the AI uses tier n. |
| Seat filling | room | A person alone for 3 seconds gets an AI opponent; it leaves when a second person joins. |
| Draft | `cardTable({ duels })` | After each duel but the last, pick one of three cards into your deck. |
| Playtest | `npm test` | Four playtests that together simulate 17 minutes of duels finish in under half a second (Apple M2 Pro, Node 22). |

## 1. Start the template

```sh
npm create netforge@latest my-cards -- --template cards --yes
cd my-cards
npm run play -- --players 2
```

With two tabs you duel yourself; with one, an AI scribe sits down after three seconds.

## 2. Add a card

```ts title=src/game.ts
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("ink_storm", { model: "style.lowpoly.meteor", kind: "spell", cost: 4, effect: { kind: "damage", amount: 2, target: "all_enemies" } });
```

Add the card's name to `src/names.json` under `weapons` (`{ "name": "Mirror Moth", "line": "..." }`). The draft offers every declared card unless the table's `pool` lists some.

## 3. Shape the table and the AI

```ts title=src/game.ts
import { cardTable } from "@netforge/engine";

cardTable({
  health: 20,
  startHand: 3,
  handMax: 7,
  boardMax: 5,
  manaMax: 10,
  turnSeconds: 30,
  draftSeconds: 15,
  duels: 4,
  starter: ["blot_imp", "blot_imp", "quill_sprite", "quill_sprite", "ink_frog", "ink_frog", "margin_bat", "ledger_crab", "serif_fox", "splash", "splash", "mirror_moth"],
  ai: [
    { health: 14, smart: 0.3 },
    { health: 16, smart: 0.55 },
    { health: 20, smart: 0.75 },
    { health: 24, smart: 0.9 },
  ],
});
```

Lower `health` makes faster duels and more `duels` a longer run. Raise each tier's `health` and `smart` for a harder gauntlet. The starter deck must hold 8 to 30 declared cards, at least one unit and some cards costing 2 or less; the build tells you which rule a deck breaks.

## 4. Reward the right things

```ts title=src/game.ts
import { earn } from "@netforge/engine";

earn.rule("ink_master", { when: "ai.beaten", n: 3, pay: 15, oncePer: "run" });
```

Events are `duel.won` (against a person, never a forfeit), `ai.beaten` and `run.won`. The AI never earns. Locally every payout is play money.

## 5. Test it

```sh
npm test
```

The playtest plays duels between two players and one person against the AI, and checks that duels are decided, a run ends and starts over, and no softlock happens.

## How it works

The card battler is a rules genre: a deterministic module with integer state and a seeded random number generator, run by the room at 30 Hz. A play, an attack, an end of turn and a draft pick are actions the server validates (whose turn it is, the card in that slot, the mana, guards). Your cursor is predicted so pointing never waits; everything else comes from the server. The room sends each player their own private state (their hand, their draft offer) separately from the public board, so another player's client never receives your cards. Because every shuffle comes from the room's seeded generator, a whole run replays exactly from its inputs. See [Make a turn-based multiplayer game](https://forge.netnet.capital/docs/guides/turn-based-browser-game.md) for the rules-genre model, and the [card battler template page](https://forge.netnet.capital/docs/templates/card-battler.md) for every file.

## Alternatives

- **[boardgame.io](https://github.com/boardgameio/boardgame.io)** is an MIT-licensed engine for turn-based games in JavaScript. It keeps game state in sync across clients, server and storage, supports game phases with different rules and turn orders per phase, generates bots that can play your game, and includes a lobby for matchmaking. It is view-layer agnostic, so you bring your own rendering. Its latest npm release is 0.50.2, published on 2022-11-10 ([npm registry](https://registry.npmjs.org/boardgame.io)). Pick it when you want a small, proven state model for a 2D board or card game and are happy to write the UI.
- **[Colyseus](https://docs.colyseus.io/)** is an authoritative Node.js room server with matchmaking and binary delta state sync. Pick it when you want to write the card rules as server code yourself and use any client.

Pick Net Forge when you want a finished, rendered card battler with AI opponents and server-side hidden information, and a declarative file your agent can extend.

## FAQ

### How do I keep players from seeing each other's hands?

The template does it on the server: the deck is shuffled by the room's seeded generator, the order never leaves the server, and each hand is sent only to its owner. Nothing in `src/game.ts` can change that.

### Can players play against the computer?

Yes. A person alone gets an AI opponent after three seconds. Its strength per duel comes from `cardTable({ ai })`, from 1 to 12 tiers of `{ health, smart }`.

### Is there a turn timer?

Yes, `turnSeconds` (30 in the template). When it runs out, the turn ends.

### Can I make it two players only, with no AI?

The table seats two players. With a second person in the room the AI leaves; a duel against a person is the only one that counts as `duel.won`.

### Can I make a deckbuilder roguelike instead?

The template already drafts one of three cards after each duel of a run. Raise `duels` and tune the AI tiers for a longer climb.

### Does the AI cheat by seeing my hand?

No. The AI picks its moves from its own hand and the cards on the board; its code does not read the other player's hand or deck. It plays through the same rules module and never earns.
