Four characters a side. One player each. Speed decides who moves.
PacketBrawl is a 1v1 tactical battler set in the Soultale world. Each player brings a squad of four characters. Characters act one at a time, in an order that speed decides across both squads together, so a fast character can act twice before a slow one acts once. Both players see the order coming.
A match ends when one squad has nobody left standing.
A hotseat match is playable in a browser. Two players, one keyboard, state in memory. That is M1, and it is what the rest of the build order sits on.
pnpm install && pnpm devpackages/simholds the whole game: turn order, damage, crit, abilities, resources, targeting, a state hash, replay, and an event stream.apps/webdraws it. Four characters a side, a turn forecast rail that is exact rather than estimated, and a rolling combat log.- 187 tests pass, and no browser appears in any of them.
The characters are real. packages/content holds eight from the published
Soultale chapters, each citing where it came from. The pack is hashed into a
version every match records, so a balance change cannot make an old replay
resolve under new numbers in silence.
Nothing in it references plot. A soul, a fighting style and a name are things a reader already has; an event is not, and the game is read by people who are behind on the story. The portrait slots are drawn and still empty, waiting on art rather than on a decision.
There is still no network, no account, and no second machine.
packages/sim is the whole of the game's logic, and it depends on nothing.
No React, no DOM, no Next.js, no Supabase. No clock, and no random source that a state does not carry. The client imports it to draw a match and to show a move at once. The server imports the same module to decide what actually happened. One implementation of the rules, never two.
The rule is structural rather than advisory:
- The sources are a TypeScript project with no ambient types, so
process,Buffer, anddocumentdo not compile there. - A test reads every source file and refuses any import that is not a file beside it. A list of forbidden packages would miss the package nobody thought of. Allowing only a sibling file misses none.
- The same test refuses
Math.random,Date.now, andnew Date.
If a change to the rules needs a React file touched, the boundary is broken. Fix the boundary.
Node 24 and pnpm are needed. The Node version is in .nvmrc.
pnpm installRun everything the way CI runs it:
pnpm verifyThat is lint, type check, tests, then the build. The parts run alone too:
pnpm testpnpm typecheckpnpm formatRun the game itself:
pnpm devEvery character walks the same distance to reach a turn. Speed sets how fast they walk, so a character with twice the speed arrives twice as often. Nothing takes a turn in rotation, and nothing is owed a turn because the other side just had one.
A character of speed 250 reaches turns at 40, 80, and 120. A character of speed 100 reaches one at 100. So the fast character acts twice, and only then does the slow one act at all.
The distance is a whole number rather than 10000 divided by speed. A fraction would be hashed and compared on two machines, and two machines do not have to agree about the last bit of one.
When two characters arrive together, the side that has waited longer goes first. Without that rule one squad takes every tie for a whole match.
Nothing acts outside this order. There is no ability that interrupts, so the list of upcoming turns is a promise rather than an estimate, and a test holds it to what the match goes on to do.
PacketBrawl/
├── apps/
│ └── web/ the interface. imports the sim.
│ ├── src/lib/ what the screen shows, as pure functions
│ ├── src/components/
│ └── test/
├── packages/
│ ├── sim/ the rules. depends on nothing.
│ │ ├── src/
│ │ └── test/
│ └── content/ the characters. hashed into a version.
├── docs/
│ ├── architecture.md how the pieces fit, and why
│ ├── combat.md how a hit resolves, and why
│ ├── deployment.md where it runs, and the settings not in git
│ └── milestones.md the build order, and what changed from the spec
├── scripts/
│ └── check-links.sh every relative link in every document
└── .github/workflows/ lint, type check, test, CodeQL
packages/bot does not exist yet. It arrives at the milestone that needs it,
rather than standing empty until then.
pnpm testThe rules engine needs no browser, which is the whole reason it is kept apart from anything that draws it.
Two kinds of test are worth naming:
- Recorded values. The generator's output and a state hash are pinned to the values this implementation produces. A stored match replays through both, so changing either invalidates every match recorded before the change. That has to be a decision somebody makes, not a side effect of a refactor.
- The forecast against reality. The order shown to players is checked against the order the match actually takes, over several speed pairings.
Read CONTRIBUTING.md first, and its licence section before anything else, because this project is not open source and that changes what contributing means.
AGENTS.md is the full set of rules: how commits are split, how text in this repository is written, and who signs a commit.
For help, see SUPPORT.md. To report a security problem privately, see SECURITY.md.
PacketBrawl is not open source. Copyright (c) 2025-2026 Stiven Gjekaj, all rights reserved. See LICENSE and TERMS.md.
You may read this source and fork it inside GitHub, because GitHub's Terms of Service give every GitHub user that right. You may not use, copy, modify, or distribute it without written permission. Reading the source grants you no licence to it.