@netforge/engine 0.0.0-stage → 0.1.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.
Files changed (244) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +147 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +208 -0
  76. package/dist/net/protocol.js +8 -0
  77. package/dist/net/room-client.d.ts +57 -0
  78. package/dist/net/room-client.js +144 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms.txt +183 -0
  244. package/package.json +82 -3
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The card battler genre (template "cards"): one against one, a person
3
+ * against a person or against the AI (a seat bot that fills the second
4
+ * seat after a player has been alone a few seconds, and never earns). A
5
+ * run is a gauntlet of duels with a draft between them.
6
+ *
7
+ * Hidden information stays on the server: decks are shuffled here from the
8
+ * room's seeded PRNG, the deck order never leaves this module, and a hand
9
+ * (and a draft offer) appears only in its owner's view. Snapshots carry the
10
+ * avatars (the quills) only; the public view carries counts.
11
+ */
12
+ import type { GameManifest } from "../api/registry.js";
13
+ import { type CardDef, type CardTableDef } from "../api/cards.js";
14
+ import type { RulesContext, RulesGenre } from "./kit.js";
15
+ /** Actions (RulesInput.act). */
16
+ export declare const CardAct: {
17
+ readonly Play: 1;
18
+ readonly Attack: 2;
19
+ readonly End: 3;
20
+ readonly Draft: 4;
21
+ };
22
+ /** Target codes (RulesInput.b): 0 none, 1 the enemy hero, 2 your hero, 10+i enemy unit i, 20+i your unit i. */
23
+ export declare const Target: {
24
+ readonly None: 0;
25
+ readonly EnemyHero: 1;
26
+ readonly OwnHero: 2;
27
+ readonly EnemyUnit: 10;
28
+ readonly OwnUnit: 20;
29
+ };
30
+ interface Unit {
31
+ type: number;
32
+ attack: number;
33
+ health: number;
34
+ maxHealth: number;
35
+ guard: boolean;
36
+ /** May attack this turn. */
37
+ ready: boolean;
38
+ }
39
+ interface Side {
40
+ player: number;
41
+ hero: number;
42
+ heroMax: number;
43
+ mana: number;
44
+ maxMana: number;
45
+ /** Server only: the shuffled deck, top first. Never sent. */
46
+ deck: number[];
47
+ hand: number[];
48
+ board: Unit[];
49
+ fatigue: number;
50
+ }
51
+ interface Duel {
52
+ sides: [Side, Side];
53
+ /** Index into sides of whose turn it is. */
54
+ turn: 0 | 1;
55
+ turnNo: number;
56
+ timer: number;
57
+ /** A bot's think clock: ticks since the turn began. */
58
+ turnTicks: number;
59
+ /** The AI tier when one side is a bot, else -1. */
60
+ tier: number;
61
+ }
62
+ interface CardPlayer {
63
+ /** The player's deck list (their collection), card type indices. */
64
+ list: number[];
65
+ wins: number;
66
+ losses: number;
67
+ draws: number;
68
+ aiWins: number;
69
+ /** Draft offer (private), or null. */
70
+ offer: number[] | null;
71
+ joinedAt: number;
72
+ }
73
+ interface LogLine {
74
+ p: number;
75
+ kind: "play" | "attack" | "win" | "draw" | "end";
76
+ card: number;
77
+ target: number;
78
+ }
79
+ export interface CardsState {
80
+ rng: {
81
+ s: number;
82
+ };
83
+ ctx: RulesContext;
84
+ tick: number;
85
+ stage: "waiting" | "duel" | "draft" | "done";
86
+ /** Duels started this run (1-based once one has). */
87
+ duelNo: number;
88
+ duel: Duel | null;
89
+ /** Ticks left in the draft or the pause before the next duel. */
90
+ countdown: number;
91
+ players: Map<number, CardPlayer>;
92
+ /** Ticks a person has been alone (a seat bot comes after a few seconds). */
93
+ alone: number;
94
+ log: LogLine[];
95
+ finished: number;
96
+ draws: number;
97
+ peakBoard: number;
98
+ activity: number;
99
+ /** Turns of every finished duel (checks). */
100
+ turnsPlayed: number[];
101
+ }
102
+ export interface CompiledCards {
103
+ readonly table: CardTableDef;
104
+ readonly cards: readonly {
105
+ readonly id: string;
106
+ readonly def: CardDef;
107
+ }[];
108
+ readonly starter: readonly number[];
109
+ readonly pool: readonly number[];
110
+ }
111
+ export declare function compileCards(m: GameManifest): CompiledCards;
112
+ export declare function cardsGenre(m: GameManifest): RulesGenre<CardsState> & {
113
+ readonly game: CompiledCards;
114
+ };
115
+ export {};
@@ -0,0 +1,547 @@
1
+ import { cardsContent } from "../api/cards.js";
2
+ import { randInt, shuffle, UNITS } from "./kit.js";
3
+ /** Actions (RulesInput.act). */
4
+ export const CardAct = { Play: 1, Attack: 2, End: 3, Draft: 4 };
5
+ /** Target codes (RulesInput.b): 0 none, 1 the enemy hero, 2 your hero, 10+i enemy unit i, 20+i your unit i. */
6
+ export const Target = { None: 0, EnemyHero: 1, OwnHero: 2, EnemyUnit: 10, OwnUnit: 20 };
7
+ export function compileCards(m) {
8
+ const c = cardsContent(m);
9
+ const ids = c.cards.map(([id]) => id);
10
+ return {
11
+ table: c.table,
12
+ cards: c.cards.map(([id, def]) => ({ id, def })),
13
+ starter: c.table.starter.map((id) => ids.indexOf(id)),
14
+ pool: (c.table.pool ?? ids).map((id) => ids.indexOf(id)),
15
+ };
16
+ }
17
+ const TICK = 30;
18
+ const ALONE_TICKS = 3 * TICK;
19
+ const PAUSE_TICKS = 3 * TICK;
20
+ export function cardsGenre(m) {
21
+ const g = compileCards(m);
22
+ const t = g.table;
23
+ const tickRate = TICK;
24
+ const AVATAR_STEP = Math.round((30 * UNITS) / tickRate);
25
+ const HALF_W = 9;
26
+ const HALF_H = 6;
27
+ const def = (type) => g.cards[type]?.def;
28
+ /** A card's rough worth (the AI's and the draft's sense of value). */
29
+ const worth = (type) => {
30
+ const d = def(type);
31
+ if (!d)
32
+ return 0;
33
+ const fx = d.effect ? d.effect.amount * (d.effect.kind === "draw" ? 2 : d.effect.target === "all_enemies" || d.effect.target === "all_own" ? 2.5 : 1.2) : 0;
34
+ return d.attack + d.health + fx + (d.guard ? 1 : 0) + (d.rush ? 1 : 0);
35
+ };
36
+ const people = (s) => [...s.players.keys()].filter((p) => !s.ctx.isBot(p));
37
+ const log = (s, l) => {
38
+ s.log.push(l);
39
+ if (s.log.length > 8)
40
+ s.log.shift();
41
+ };
42
+ const draw = (s, side, n) => {
43
+ for (let i = 0; i < n; i++) {
44
+ const c = side.deck.shift();
45
+ if (c === undefined) {
46
+ side.fatigue++;
47
+ side.hero -= side.fatigue;
48
+ }
49
+ else if (side.hand.length < t.handMax)
50
+ side.hand.push(c);
51
+ }
52
+ };
53
+ const startDuel = (s) => {
54
+ const ps = people(s);
55
+ const bots = [...s.players.keys()].filter((p) => s.ctx.isBot(p));
56
+ let pair = null;
57
+ if (ps.length >= 2)
58
+ pair = [ps[0], ps[1]];
59
+ else if (ps.length === 1 && bots.length >= 1)
60
+ pair = [ps[0], bots[0]];
61
+ if (!pair) {
62
+ s.stage = "waiting";
63
+ return;
64
+ }
65
+ s.duelNo++;
66
+ const vsBot = pair.some((p) => s.ctx.isBot(p));
67
+ const tier = vsBot ? Math.min(t.ai.length - 1, s.players.get(pair[0])?.aiWins ?? 0) : -1;
68
+ // Who goes first: the seeded coin.
69
+ if (randInt(s.rng, 2) === 1)
70
+ pair = [pair[1], pair[0]];
71
+ const mk = (p, idx) => {
72
+ const pl = s.players.get(p);
73
+ const bot = s.ctx.isBot(p);
74
+ const heroMax = bot ? (t.ai[tier]?.health ?? t.health) : t.health + Math.floor(s.ctx.bonus(p, "maxHealth"));
75
+ const side = { player: p, hero: heroMax, heroMax, mana: 0, maxMana: 0, deck: shuffle(s.rng, [...pl.list]), hand: [], board: [], fatigue: 0 };
76
+ draw(s, side, t.startHand + idx + Math.floor(s.ctx.bonus(p, "amount")));
77
+ return side;
78
+ };
79
+ s.duel = { sides: [mk(pair[0], 0), mk(pair[1], 1)], turn: 0, turnNo: 0, timer: 0, turnTicks: 0, tier };
80
+ s.stage = "duel";
81
+ beginTurn(s, s.duel);
82
+ s.activity++;
83
+ };
84
+ const beginTurn = (s, d) => {
85
+ d.turnNo++;
86
+ const side = d.sides[d.turn];
87
+ side.maxMana = Math.min(t.manaMax, side.maxMana + 1);
88
+ side.mana = side.maxMana + (d.turnNo <= 2 ? Math.floor(s.ctx.bonus(side.player, "haste")) : 0);
89
+ for (const u of side.board)
90
+ u.ready = true;
91
+ if (d.turnNo > 1)
92
+ draw(s, side, 1);
93
+ d.timer = Math.round(t.turnSeconds * tickRate);
94
+ d.turnTicks = 0;
95
+ };
96
+ const endDuel = (s, winner, emit, forfeit = false) => {
97
+ const d = s.duel;
98
+ if (!d)
99
+ return;
100
+ s.turnsPlayed.push(d.turnNo);
101
+ const [a, b] = d.sides;
102
+ const vsBot = s.ctx.isBot(a.player) || s.ctx.isBot(b.player);
103
+ if (winner === null) {
104
+ s.draws++;
105
+ for (const side of d.sides) {
106
+ const pl = s.players.get(side.player);
107
+ if (pl)
108
+ pl.draws++;
109
+ }
110
+ log(s, { p: 0, kind: "draw", card: 0, target: 0 });
111
+ }
112
+ else {
113
+ const loser = winner === a.player ? b.player : a.player;
114
+ const w = s.players.get(winner);
115
+ const l = s.players.get(loser);
116
+ if (w)
117
+ w.wins++;
118
+ if (l)
119
+ l.losses++;
120
+ log(s, { p: winner, kind: "win", card: 0, target: 0 });
121
+ if (!s.ctx.isBot(winner)) {
122
+ if (vsBot) {
123
+ if (w)
124
+ w.aiWins++;
125
+ // PvE: beating the AI pays the gauntlet's rules (a forfeit by a leaving bot pays nothing).
126
+ emit(forfeit ? { name: "ai.beaten", players: [winner], unpaid: true } : { name: "ai.beaten", players: [winner], own: true });
127
+ }
128
+ else {
129
+ // PvP: a duel won against a person; a forfeit (the other walked out) pays nothing.
130
+ emit(forfeit ? { name: "duel.won", players: [winner], unpaid: true } : { name: "duel.won", players: [winner], own: true });
131
+ }
132
+ }
133
+ }
134
+ s.finished++;
135
+ s.duel = null;
136
+ if (s.duelNo >= t.duels) {
137
+ s.stage = "done";
138
+ for (const [p, pl] of s.players)
139
+ if (!s.ctx.isBot(p) && pl.wins > pl.losses)
140
+ emit({ name: "run.won", players: [p], own: true });
141
+ }
142
+ else {
143
+ s.stage = "draft";
144
+ s.countdown = Math.round(t.draftSeconds * tickRate);
145
+ for (const [p, pl] of s.players)
146
+ pl.offer = offerFor(s, p);
147
+ }
148
+ s.activity++;
149
+ };
150
+ const offerFor = (s, _p) => {
151
+ const out = [];
152
+ for (let i = 0; i < 3 && g.pool.length > 0; i++) {
153
+ let c = g.pool[randInt(s.rng, g.pool.length)];
154
+ for (let k = 0; k < 4 && out.includes(c); k++)
155
+ c = g.pool[randInt(s.rng, g.pool.length)];
156
+ out.push(c);
157
+ }
158
+ return out;
159
+ };
160
+ const sideOf = (d, p) => {
161
+ if (d.sides[0].player === p)
162
+ return { me: d.sides[0], foe: d.sides[1], idx: 0 };
163
+ if (d.sides[1].player === p)
164
+ return { me: d.sides[1], foe: d.sides[0], idx: 1 };
165
+ return null;
166
+ };
167
+ const dealDamage = (side, target, amount) => {
168
+ if (target === Target.EnemyHero || target === Target.OwnHero)
169
+ side.hero -= amount;
170
+ else {
171
+ const u = side.board[target % 10];
172
+ if (u)
173
+ u.health -= amount;
174
+ }
175
+ };
176
+ const sweep = (d) => {
177
+ for (const side of d.sides)
178
+ side.board = side.board.filter((u) => u.health > 0);
179
+ };
180
+ /** Applies an effect; false when its target is not valid (the card is not played). */
181
+ const effect = (s, d, me, foe, fx, target) => {
182
+ if (fx.kind === "draw") {
183
+ draw(s, me, fx.amount);
184
+ return true;
185
+ }
186
+ if (fx.kind === "heal") {
187
+ me.hero = Math.min(me.heroMax, me.hero + fx.amount);
188
+ return true;
189
+ }
190
+ if (fx.kind === "damage") {
191
+ if (fx.target === "all_enemies") {
192
+ for (const u of foe.board)
193
+ u.health -= fx.amount;
194
+ }
195
+ else if (fx.target === "enemy_hero")
196
+ foe.hero -= fx.amount;
197
+ else if (fx.target === "own_hero")
198
+ me.hero -= fx.amount;
199
+ else {
200
+ if (target === Target.EnemyHero)
201
+ foe.hero -= fx.amount;
202
+ else if (target >= Target.EnemyUnit && target < Target.OwnUnit && foe.board[target - Target.EnemyUnit])
203
+ dealDamage(foe, target, fx.amount);
204
+ else
205
+ return false;
206
+ }
207
+ sweep(d);
208
+ return true;
209
+ }
210
+ // buff
211
+ if (fx.target === "all_own") {
212
+ for (const u of me.board) {
213
+ u.attack += fx.amount;
214
+ u.health += fx.amount;
215
+ u.maxHealth += fx.amount;
216
+ }
217
+ return true;
218
+ }
219
+ const u = target >= Target.OwnUnit ? me.board[target - Target.OwnUnit] : undefined;
220
+ if (!u)
221
+ return me.board.length === 0;
222
+ u.attack += fx.amount;
223
+ u.health += fx.amount;
224
+ u.maxHealth += fx.amount;
225
+ return true;
226
+ };
227
+ const settle = (s, d, emit) => {
228
+ const [a, b] = d.sides;
229
+ if (a.hero <= 0 || b.hero <= 0)
230
+ endDuel(s, a.hero <= 0 && b.hero <= 0 ? null : a.hero <= 0 ? b.player : a.player, emit);
231
+ };
232
+ const play = (s, d, me, foe, slot, type, target, emit) => {
233
+ const c = me.hand[slot];
234
+ const cd = c === undefined ? undefined : def(c);
235
+ if (c === undefined || c !== type || !cd || cd.cost > me.mana)
236
+ return;
237
+ if (cd.kind === "unit" && me.board.length >= t.boardMax)
238
+ return;
239
+ if (cd.kind === "spell" && cd.effect && !effect(s, d, me, foe, cd.effect, target))
240
+ return;
241
+ me.hand.splice(slot, 1);
242
+ me.mana -= cd.cost;
243
+ if (cd.kind === "unit") {
244
+ const might = Math.floor(s.ctx.bonus(me.player, "might"));
245
+ me.board.push({ type: c, attack: cd.attack + might, health: cd.health, maxHealth: cd.health, guard: cd.guard, ready: cd.rush });
246
+ if (cd.effect)
247
+ effect(s, d, me, foe, cd.effect, target);
248
+ }
249
+ log(s, { p: me.player, kind: "play", card: c, target });
250
+ s.activity++;
251
+ s.peakBoard = Math.max(s.peakBoard, me.board.length);
252
+ settle(s, d, emit);
253
+ };
254
+ const attack = (s, d, me, foe, from, target, emit) => {
255
+ const u = me.board[from];
256
+ if (!u || !u.ready || u.attack <= 0)
257
+ return;
258
+ const guards = foe.board.some((x) => x.guard);
259
+ if (target === Target.EnemyHero) {
260
+ if (guards)
261
+ return;
262
+ foe.hero -= u.attack;
263
+ }
264
+ else {
265
+ const v = foe.board[target - Target.EnemyUnit];
266
+ if (target < Target.EnemyUnit || target >= Target.OwnUnit || !v)
267
+ return;
268
+ if (guards && !v.guard)
269
+ return;
270
+ v.health -= u.attack;
271
+ u.health -= v.attack;
272
+ }
273
+ u.ready = false;
274
+ log(s, { p: me.player, kind: "attack", card: u.type, target });
275
+ sweep(d);
276
+ s.activity++;
277
+ settle(s, d, emit);
278
+ };
279
+ const endTurn = (s, d) => {
280
+ log(s, { p: d.sides[d.turn].player, kind: "end", card: 0, target: 0 });
281
+ d.turn = d.turn === 0 ? 1 : 0;
282
+ beginTurn(s, d);
283
+ s.activity++;
284
+ };
285
+ /** The AI's next move on its turn, or an end of turn. smart: chance it takes the best move. */
286
+ const aiMove = (s, d, me, foe, smart, salt) => {
287
+ const act = (a, x, y, z) => ({ mx: 0, mz: 0, cx: 0, cz: 0, act: a, a: x, b: y, c: z });
288
+ const sloppy = ((Math.imul(salt + d.turnNo * 31 + d.turnTicks, 2654435761) >>> 0) % 1000) / 1000 >= smart;
289
+ // Play: the most valuable card it can afford (a sloppy turn plays the first it can).
290
+ const playable = me.hand
291
+ .map((c, i) => ({ c, i, d: def(c) }))
292
+ .filter((x) => x.d && x.d.cost <= me.mana && !(x.d.kind === "unit" && me.board.length >= t.boardMax))
293
+ .sort((a, b) => (sloppy ? a.i - b.i : worth(b.c) - worth(a.c)));
294
+ for (const p of playable) {
295
+ const fx = p.d?.effect;
296
+ let target = Target.None;
297
+ if (fx && (fx.target === "enemy" || fx.target === "own_unit")) {
298
+ if (fx.kind === "damage") {
299
+ // Kill the biggest enemy unit it can, else hit the hero.
300
+ const kill = foe.board.map((u, i) => ({ u, i })).filter((x) => x.u.health <= fx.amount).sort((a, b) => b.u.attack - a.u.attack)[0];
301
+ target = kill ? Target.EnemyUnit + kill.i : Target.EnemyHero;
302
+ }
303
+ else if (fx.kind === "buff") {
304
+ const best = me.board.map((u, i) => ({ u, i })).sort((a, b) => b.u.attack - a.u.attack)[0];
305
+ if (!best)
306
+ continue;
307
+ target = Target.OwnUnit + best.i;
308
+ }
309
+ }
310
+ if (fx?.kind === "heal" && me.hero > me.heroMax - 3)
311
+ continue;
312
+ return act(CardAct.Play, p.i, target, p.c);
313
+ }
314
+ // Attack: guards first; trade up when it can; else the hero.
315
+ const guards = foe.board.map((u, i) => ({ u, i })).filter((x) => x.u.guard);
316
+ for (let i = 0; i < me.board.length; i++) {
317
+ const u = me.board[i];
318
+ if (!u.ready || u.attack <= 0)
319
+ continue;
320
+ if (guards.length > 0) {
321
+ const g0 = guards.sort((a, b) => a.u.health - b.u.health)[0];
322
+ if (g0)
323
+ return act(CardAct.Attack, i, Target.EnemyUnit + g0.i, 0);
324
+ }
325
+ const trade = foe.board.map((v, j) => ({ v, j })).find((x) => x.v.health <= u.attack && x.v.attack < u.health && x.v.attack >= u.attack - 1);
326
+ if (trade && !sloppy)
327
+ return act(CardAct.Attack, i, Target.EnemyUnit + trade.j, 0);
328
+ return act(CardAct.Attack, i, Target.EnemyHero, 0);
329
+ }
330
+ return act(CardAct.End, 0, 0, 0);
331
+ };
332
+ const unitView = (u) => ({ type: u.type, attack: u.attack, health: u.health, maxHealth: u.maxHealth, guard: u.guard, ready: u.ready });
333
+ return {
334
+ template: "cards",
335
+ tickRate,
336
+ endSeconds: 8,
337
+ infoEveryTicks: 3,
338
+ game: g,
339
+ create(seed, ctx) {
340
+ return { rng: { s: seed | 0 }, ctx, tick: 0, stage: "waiting", duelNo: 0, duel: null, countdown: PAUSE_TICKS, players: new Map(), alone: 0, log: [], finished: 0, draws: 0, peakBoard: 0, activity: 0, turnsPlayed: [] };
341
+ },
342
+ spawn(_s, player) {
343
+ return { x: Math.round(((player % 2 === 0 ? 3 : -3) + (player % 5) * 0.2) * UNITS), z: Math.round((player % 2 === 0 ? -2 : 2) * UNITS) };
344
+ },
345
+ moveAvatar(a, i) {
346
+ if (i.cx === 0 && i.cz === 0 && i.act === 0)
347
+ return;
348
+ const tx = Math.round(Math.max(-HALF_W, Math.min(HALF_W, i.cx / 16)) * UNITS);
349
+ const tz = Math.round(Math.max(-HALF_H, Math.min(HALF_H, i.cz / 16)) * UNITS);
350
+ const dx = tx - a.x;
351
+ const dz = tz - a.z;
352
+ const d = Math.sqrt(dx * dx + dz * dz);
353
+ if (d < 1)
354
+ return;
355
+ if (d <= AVATAR_STEP) {
356
+ a.x += dx;
357
+ a.z += dz;
358
+ }
359
+ else {
360
+ a.x += Math.round((dx * AVATAR_STEP) / d);
361
+ a.z += Math.round((dz * AVATAR_STEP) / d);
362
+ }
363
+ },
364
+ join(s, player) {
365
+ s.players.set(player, { list: [...g.starter], wins: 0, losses: 0, draws: 0, aiWins: 0, offer: null, joinedAt: s.tick });
366
+ s.activity++;
367
+ },
368
+ leave(s, player) {
369
+ s.players.delete(player);
370
+ const d = s.duel;
371
+ if (d && sideOf(d, player)) {
372
+ const other = d.sides[0].player === player ? d.sides[1].player : d.sides[0].player;
373
+ if (s.ctx.isBot(player)) {
374
+ // The seat bot made room for a person: the duel is called off, no result.
375
+ s.duel = null;
376
+ s.duelNo = Math.max(0, s.duelNo - 1);
377
+ s.stage = "waiting";
378
+ s.countdown = PAUSE_TICKS;
379
+ }
380
+ else {
381
+ endDuel(s, other, () => undefined, true);
382
+ }
383
+ }
384
+ s.activity++;
385
+ },
386
+ act(s, player, i, _a, emit) {
387
+ const pl = s.players.get(player);
388
+ if (!pl)
389
+ return;
390
+ if (i.act === CardAct.Draft) {
391
+ if (s.stage !== "draft" || !pl.offer)
392
+ return;
393
+ const c = pl.offer[i.a];
394
+ if (c === undefined)
395
+ return;
396
+ pl.list.push(c);
397
+ pl.offer = null;
398
+ s.activity++;
399
+ return;
400
+ }
401
+ const d = s.duel;
402
+ if (!d || s.stage !== "duel")
403
+ return;
404
+ const sd = sideOf(d, player);
405
+ if (!sd || d.turn !== sd.idx)
406
+ return;
407
+ if (i.act === CardAct.Play)
408
+ play(s, d, sd.me, sd.foe, i.a, i.c, i.b, emit);
409
+ else if (i.act === CardAct.Attack)
410
+ attack(s, d, sd.me, sd.foe, i.a, i.b, emit);
411
+ else if (i.act === CardAct.End)
412
+ endTurn(s, d);
413
+ },
414
+ step(s, emit) {
415
+ s.tick++;
416
+ const ps = people(s);
417
+ s.alone = ps.length === 1 ? s.alone + 1 : 0;
418
+ if (s.stage === "waiting" || s.stage === "draft") {
419
+ if (s.stage === "draft" && [...s.players].every(([p, pl]) => s.ctx.isBot(p) || !pl.offer))
420
+ s.countdown = Math.min(s.countdown, PAUSE_TICKS / 3);
421
+ if (s.countdown > 0)
422
+ s.countdown--;
423
+ else {
424
+ // Unpicked offers take the best card.
425
+ for (const pl of s.players.values()) {
426
+ if (pl.offer) {
427
+ const best = [...pl.offer].sort((a, b) => worth(b) - worth(a))[0];
428
+ if (best !== undefined)
429
+ pl.list.push(best);
430
+ pl.offer = null;
431
+ }
432
+ }
433
+ startDuel(s);
434
+ if (s.stage === "waiting")
435
+ s.countdown = TICK;
436
+ }
437
+ return;
438
+ }
439
+ const d = s.duel;
440
+ if (s.stage !== "duel" || !d)
441
+ return;
442
+ d.turnTicks++;
443
+ if (--d.timer <= 0)
444
+ endTurn(s, d);
445
+ if (d.turnNo > t.turnCap)
446
+ endDuel(s, null, emit);
447
+ },
448
+ outcome(s) {
449
+ if (s.stage !== "done")
450
+ return null;
451
+ return [...s.players].some(([p, pl]) => !s.ctx.isBot(p) && pl.wins > pl.losses) ? "won" : "lost";
452
+ },
453
+ seats(s, humans) {
454
+ return humans === 1 && s.alone >= ALONE_TICKS ? 1 : 0;
455
+ },
456
+ view(s, player) {
457
+ const d = s.duel;
458
+ const sideView = (side) => ({
459
+ player: side.player,
460
+ hero: side.hero,
461
+ heroMax: side.heroMax,
462
+ mana: side.mana,
463
+ maxMana: side.maxMana,
464
+ hand: side.hand.length,
465
+ deck: side.deck.length,
466
+ board: side.board.map(unitView),
467
+ });
468
+ const me = player !== null ? s.players.get(player) : undefined;
469
+ const mySide = d && player !== null ? sideOf(d, player)?.me : undefined;
470
+ return {
471
+ genre: "cards",
472
+ stage: s.stage,
473
+ duelNo: s.duelNo,
474
+ duels: t.duels,
475
+ countdown: Math.ceil(s.countdown / tickRate),
476
+ duel: d ? { turn: d.sides[d.turn].player, turnNo: d.turnNo, timer: Math.ceil(d.timer / tickRate), tier: d.tier, sides: d.sides.map(sideView) } : null,
477
+ log: s.log.map((l) => ({ ...l })),
478
+ players: [...s.players].map(([id, p]) => ({ id, bot: s.ctx.isBot(id), wins: p.wins, losses: p.losses, draws: p.draws, deck: p.list.length })),
479
+ // What only this player may see: their hand, their draft offer, their own deck list (sorted, never its order in play).
480
+ ...(me ? { hand: mySide ? [...mySide.hand] : [], offer: me.offer ? [...me.offer] : null, list: [...me.list].sort((a, b) => a - b) } : {}),
481
+ };
482
+ },
483
+ entities(_s, _out) {
484
+ // Nothing but the avatars is networked: every card lives in run state.
485
+ },
486
+ bot(s, player, a, tick, skill) {
487
+ const idle = { mx: 0, mz: 0, cx: Math.round((a.x / UNITS) * 16), cz: Math.round((a.z / UNITS) * 16), act: 0, a: 0, b: 0, c: 0 };
488
+ const pl = s.players.get(player);
489
+ if (!pl)
490
+ return idle;
491
+ if (s.stage === "draft" && pl.offer) {
492
+ if ((tick + player) % 20 !== 0)
493
+ return idle;
494
+ const best = pl.offer.map((c, i) => ({ c, i })).sort((x, y) => worth(y.c) - worth(x.c))[0];
495
+ return { ...idle, act: CardAct.Draft, a: best?.i ?? 0 };
496
+ }
497
+ const d = s.duel;
498
+ if (!d)
499
+ return idle;
500
+ const sd = sideOf(d, player);
501
+ if (!sd || d.turn !== sd.idx)
502
+ return idle;
503
+ // Think a moment at the start of a turn, then a move every few ticks.
504
+ const think = Math.round(tickRate * (skill >= 1 ? 0.4 : 1.2));
505
+ if (d.turnTicks < think || (d.turnTicks - think) % 6 !== 0)
506
+ return idle;
507
+ const smart = s.ctx.isBot(player) ? (t.ai[Math.max(0, d.tier)]?.smart ?? 0.5) : skill;
508
+ return aiMove(s, d, sd.me, sd.foe, smart, player * 7919 + tick);
509
+ },
510
+ stats(s) {
511
+ let top = 0;
512
+ let scorers = 0;
513
+ let wins = 0;
514
+ let losses = 0;
515
+ for (const [p, pl] of s.players) {
516
+ if (s.ctx.isBot(p))
517
+ continue;
518
+ top = Math.max(top, pl.wins);
519
+ if (pl.wins > 0)
520
+ scorers++;
521
+ wins += pl.wins;
522
+ losses += pl.losses;
523
+ }
524
+ return { kills: wins, wave: Math.max(1, s.duelNo), level: top, deaths: losses, peakUnits: s.peakBoard, scorers, topScore: top };
525
+ },
526
+ activity(s) {
527
+ return s.activity;
528
+ },
529
+ checks(run) {
530
+ const min = Math.round(run.seconds / 6) / 10;
531
+ const duels = run.stats.kills + run.stats.deaths;
532
+ return [
533
+ {
534
+ ok: duels > 0 || run.seconds < 120,
535
+ line: `no duel was decided in ${min} minutes: lower hero health (cardTable health), raise unit attack or cheapen the cards`,
536
+ },
537
+ {
538
+ ok: run.stats.wave >= 2 || run.seconds < 200,
539
+ line: `only ${run.stats.wave} duel(s) started in ${min} minutes: duels run too long (fewer turns to win: lower hero health, more damage)`,
540
+ },
541
+ ];
542
+ },
543
+ progress(run) {
544
+ return Math.min(1, run.stats.wave / Math.max(1, t.duels)) * 0.7 + (run.stats.kills > 0 ? 0.3 : 0);
545
+ },
546
+ };
547
+ }