@pkmn/engine 0.1.0-dev.14ca6108 → 0.1.0-dev.15f64810

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 (48) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +44 -19
  3. package/build/pkg/addon.js +6 -7
  4. package/build/pkg/addon.js.map +1 -1
  5. package/build/pkg/data.d.ts +4 -4
  6. package/build/pkg/data.js.map +1 -1
  7. package/build/pkg/gen1.js.map +1 -1
  8. package/build/pkg/index.d.ts +2 -1
  9. package/build/pkg/index.js.map +1 -1
  10. package/build/pkg/protocol.js +5 -5
  11. package/build/pkg/protocol.js.map +1 -1
  12. package/build/test/display.js +2 -2
  13. package/build/test/display.js.map +1 -1
  14. package/build/tools/debug.js +5 -4
  15. package/build/tools/debug.js.map +1 -1
  16. package/build.zig +100 -102
  17. package/package.json +34 -23
  18. package/src/bin/install-pkmn-engine +26 -18
  19. package/src/lib/common/data.zig +8 -8
  20. package/src/lib/common/debug.zig +2 -2
  21. package/src/lib/common/optional.zig +1 -1
  22. package/src/lib/common/options.zig +2 -0
  23. package/src/lib/common/protocol.zig +397 -384
  24. package/src/lib/common/rational.zig +11 -8
  25. package/src/lib/common/rng.zig +20 -11
  26. package/src/lib/gen1/calc.zig +132 -29
  27. package/src/lib/gen1/chance.zig +91 -92
  28. package/src/lib/gen1/data/moves.zig +10 -10
  29. package/src/lib/gen1/data/species.zig +3 -3
  30. package/src/lib/gen1/data/types.zig +5 -5
  31. package/src/lib/gen1/data.zig +35 -35
  32. package/src/lib/gen1/helpers.zig +136 -49
  33. package/src/lib/gen1/mechanics.zig +327 -307
  34. package/src/lib/gen2/calc.zig +143 -0
  35. package/src/lib/gen2/chance.zig +1315 -0
  36. package/src/lib/gen2/data/items.zig +3 -3
  37. package/src/lib/gen2/data/moves.zig +5 -5
  38. package/src/lib/gen2/data/species.zig +1 -1
  39. package/src/lib/gen2/data/types.zig +10 -10
  40. package/src/lib/gen2/data.zig +27 -37
  41. package/src/lib/gen2/generated.zig +806 -0
  42. package/src/lib/gen2/helpers.zig +37 -7
  43. package/src/lib/gen2/mechanics.zig +1633 -484
  44. package/src/lib/{binding/node.zig → node.zig} +9 -12
  45. package/src/lib/pkmn.zig +27 -12
  46. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  47. package/src/lib/gen1/README.md +0 -894
  48. package/src/lib/gen2/README.md +0 -330
@@ -43,8 +43,7 @@ pub fn Rational(comptime T: type) type {
43
43
  /// Update the rational by multiplying its numerator by p and its denominator by q.
44
44
  /// Both p and q must be >= 1, and if computable at comptime must have no common factors.
45
45
  pub fn update(r: *Self, p: anytype, q: anytype) Error!void {
46
- // FIXME
47
- // std.debug.print("({d}/{d}) * ", .{p, q});
46
+ // std.debug.print("({d}/{d}) * ", .{ p, q }); // DEBUG
48
47
  assert(p >= 1);
49
48
  assert(q >= 1);
50
49
 
@@ -185,16 +184,16 @@ pub fn Rational(comptime T: type) type {
185
184
  try writer.print("{d}/{d}", .{ self.p, self.q });
186
185
  }
187
186
 
188
- inline fn multiplication(r: *Self, p: anytype, q: anytype) !void {
187
+ fn multiplication(r: *Self, p: anytype, q: anytype) !void {
189
188
  r.p = try std.math.mul(T, r.p, p);
190
189
  r.q = try std.math.mul(T, r.q, q);
191
190
  }
192
191
 
193
- inline fn addition(r: *Self, p: anytype, q: anytype) !void {
192
+ fn addition(r: *Self, p: anytype, q: anytype) !void {
194
193
  // (a/b) + (c/d) = (ad+bc)/(bd)
195
- var d = try std.math.mul(T, r.q, q);
196
- var n1 = try std.math.mul(T, r.p, q);
197
- var n2 = try std.math.mul(T, r.q, p);
194
+ const d = try std.math.mul(T, r.q, q);
195
+ const n1 = try std.math.mul(T, r.p, q);
196
+ const n2 = try std.math.mul(T, r.q, p);
198
197
 
199
198
  r.p = try std.math.add(T, n1, n2);
200
199
  r.q = d;
@@ -203,7 +202,7 @@ pub fn Rational(comptime T: type) type {
203
202
  }
204
203
 
205
204
  // https://en.wikipedia.org/wiki/Euclidean_algorithm
206
- inline fn gcd(p: anytype, q: anytype) @TypeOf(p, q) {
205
+ fn gcd(p: anytype, q: anytype) @TypeOf(p, q) {
207
206
  assert(p >= 1);
208
207
  assert(q >= 1);
209
208
 
@@ -234,6 +233,8 @@ test Rational {
234
233
  var r: Rational(t) = .{};
235
234
 
236
235
  var c: t = 128;
236
+ _ = &c;
237
+
237
238
  try r.update(c, 256);
238
239
  try doTurn(&r);
239
240
 
@@ -320,6 +321,8 @@ test BigRational {
320
321
  try r.reset();
321
322
 
322
323
  var c: u8 = 128;
324
+ _ = &c;
325
+
323
326
  try r.update(c, 256);
324
327
  try doTurn(&r);
325
328
 
@@ -1,6 +1,6 @@
1
1
  const std = @import("std");
2
2
 
3
- const options = @import("./options.zig");
3
+ const options = @import("options.zig");
4
4
 
5
5
  const assert = std.debug.assert;
6
6
  const expectEqual = std.testing.expectEqual;
@@ -25,6 +25,11 @@ pub fn PRNG(comptime gen: comptime_int) type {
25
25
  pub fn next(self: *Self) Output(gen) {
26
26
  return self.src.next();
27
27
  }
28
+
29
+ pub fn advance(self: *Self, n: usize) void {
30
+ if (!options.advance) return;
31
+ for (0..n) |_| self.src.advance();
32
+ }
28
33
  };
29
34
  }
30
35
 
@@ -61,11 +66,15 @@ pub const PSRNG = extern struct {
61
66
  (@as(u64, self.range(u16, 0, 0x10000)) << 16) |
62
67
  (@as(u64, self.range(u16, 0, 0x10000)));
63
68
  }
69
+
70
+ pub fn Bound(comptime T: type) type {
71
+ return std.math.IntFittingRange(0, std.math.maxInt(T) + 1);
72
+ }
64
73
  };
65
74
 
66
75
  test PSRNG {
67
76
  var psrng: PSRNG = .{ .src = .{ .seed = 0x0001000200030004 } };
68
- try expectEqual(@as(u8, 121), psrng.range(u8, 0, 256));
77
+ try expectEqual(121, psrng.range(u8, 0, 256));
69
78
  try expectEqual(false, psrng.chance(u8, 128, 256)); // 226 < 128
70
79
  }
71
80
 
@@ -89,6 +98,10 @@ pub const Gen12 = extern struct {
89
98
  self.index = (self.index + 1) % 9;
90
99
  return val;
91
100
  }
101
+
102
+ pub fn advance(self: *Gen12) void {
103
+ _ = self.next();
104
+ }
92
105
  };
93
106
 
94
107
  test Gen12 {
@@ -98,9 +111,9 @@ test Gen12 {
98
111
  try expectEqual(e, rng.next());
99
112
  }
100
113
 
101
- try expectEqual(@as(u8, 16), Gen12.percent(6) + 1);
102
- try expectEqual(@as(u8, 16), Gen12.percent(7) - 1);
103
- try expectEqual(@as(u8, 128), Gen12.percent(50) + 1);
114
+ try expectEqual(16, Gen12.percent(6) + 1);
115
+ try expectEqual(16, Gen12.percent(7) - 1);
116
+ try expectEqual(128, Gen12.percent(50) + 1);
104
117
  }
105
118
 
106
119
  // https://pkmn.cc/pokeemerald/src/random.c
@@ -188,7 +201,7 @@ pub fn FixedRNG(comptime gen: comptime_int, comptime len: usize) type {
188
201
  for (0..n) |_| _ = self.next();
189
202
  }
190
203
 
191
- pub fn range(self: *Self, comptime T: type, from: T, to: Bound(T)) T {
204
+ pub fn range(self: *Self, comptime T: type, from: T, to: PSRNG.Bound(T)) T {
192
205
  assert(showdown);
193
206
  return @intCast(@as(u64, self.next()) * (to - from) / divisor + from);
194
207
  }
@@ -197,7 +210,7 @@ pub fn FixedRNG(comptime gen: comptime_int, comptime len: usize) type {
197
210
  self: *Self,
198
211
  comptime T: type,
199
212
  numerator: T,
200
- denominator: Bound(T),
213
+ denominator: PSRNG.Bound(T),
201
214
  ) bool {
202
215
  assert(showdown);
203
216
  assert(denominator > 0);
@@ -228,7 +241,3 @@ fn Output(comptime gen: comptime_int) type {
228
241
  else => unreachable,
229
242
  };
230
243
  }
231
-
232
- fn Bound(comptime T: type) type {
233
- return std.math.IntFittingRange(0, std.math.maxInt(T) + 1);
234
- }
@@ -38,6 +38,9 @@ const Duration = chance.Duration;
38
38
 
39
39
  const Rolls = helpers.Rolls;
40
40
 
41
+ const tty = true; // DEBUG
42
+ const summary = false; // DEBUG
43
+
41
44
  /// Information relevant to damage calculation that occured during a Generation I battle `update`.
42
45
  pub const Summaries = extern struct {
43
46
  /// Relevant information for Player 1.
@@ -50,7 +53,7 @@ pub const Summaries = extern struct {
50
53
  }
51
54
 
52
55
  /// Returns the `Summary` for the given `player`.
53
- pub inline fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Summary) {
56
+ pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Summary) {
54
57
  assert(@typeInfo(@TypeOf(self)).Pointer.child == Summaries);
55
58
  return if (player == .P1) &self.p1 else &self.p2;
56
59
  }
@@ -201,6 +204,8 @@ pub const Options = struct {
201
204
  metronome: bool = false,
202
205
  };
203
206
 
207
+ pub const MAX_FRONTIER = 14;
208
+
204
209
  pub fn transitions(
205
210
  battle: anytype,
206
211
  c1: Choice,
@@ -211,8 +216,8 @@ pub fn transitions(
211
216
  ) !?Stats {
212
217
  var stats: Stats = .{};
213
218
 
214
- var actions = options.actions;
215
- var cap = options.cap;
219
+ const actions = options.actions;
220
+ const cap = options.cap;
216
221
 
217
222
  var seen = std.AutoHashMap(Actions, void).init(allocator);
218
223
  defer seen.deinit();
@@ -232,8 +237,8 @@ pub fn transitions(
232
237
  _ = try b.update(c1, c2, &opts);
233
238
  stats.updates += 1;
234
239
 
235
- var p1 = b.side(.P1);
236
- var p2 = b.side(.P2);
240
+ const p1 = b.side(.P1);
241
+ const p2 = b.side(.P2);
237
242
 
238
243
  var p: Rational(u128) = .{ .p = 0, .q = 1 };
239
244
  try frontier.append(opts.chance.actions);
@@ -247,13 +252,13 @@ pub fn transitions(
247
252
  var i: usize = 0;
248
253
  assert(frontier.items.len == 1);
249
254
  while (i < frontier.items.len) : (i += 1) {
250
- var template = frontier.items[i];
255
+ const template = frontier.items[i];
251
256
  opts.calc.overrides.durations = .{
252
257
  .p1 = template.p1.durations,
253
258
  .p2 = template.p2.durations,
254
259
  };
255
260
 
256
- try debug(writer, template, true, .{ .color = i, .bold = true, .background = true });
261
+ try debug(writer, template, true, .{ .color = i, .bold = true, .background = true, .indent = false });
257
262
 
258
263
  var a: Actions = .{ .p1 = .{ .metronome = p1_move }, .p2 = .{ .metronome = p2_move } };
259
264
 
@@ -281,7 +286,7 @@ pub fn transitions(
281
286
  for (Rolls.moveSlot(template.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
282
287
  for (Rolls.moveSlot(template.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
283
288
  for (Rolls.multiHit(template.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
284
- for (Rolls.multiHit(template.p2, p1_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
289
+ for (Rolls.multiHit(template.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
285
290
  for (Rolls.secondaryChance(template.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
286
291
  for (Rolls.secondaryChance(template.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
287
292
  for (Rolls.criticalHit(template.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
@@ -308,21 +313,21 @@ pub fn transitions(
308
313
  const p2_max: u8 = @intCast(p2_dmg.min);
309
314
  _ = cap;
310
315
 
311
- // var p1_max = if (p1_min != 0) p1_min else
312
- // try Rolls.coalesce(.P1, @intCast(u8, p1_dmg.min), &opts.calc.summaries, cap);
313
- // var p2_max =
314
- // try Rolls.coalesce(.P2, @intCast(u8, p2_dmg.min), &opts.calc.summaries, cap);
316
+ // const p1_max = if (p1_min != 0) p1_min else
317
+ // try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), &opts.calc.summaries, cap);
318
+ // const p2_max =
319
+ // try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), &opts.calc.summaries, cap);
315
320
 
316
321
  if (opts.chance.actions.matches(template)) {
317
322
  if (!opts.chance.actions.eql(a)) {
318
- try debug(writer, opts.chance.actions, false, .{ .color = i, .dim = true });
323
+ if (!summary) try debug(writer, opts.chance.actions, false, .{ .color = i, .dim = true });
319
324
 
320
325
  p1_min = p1_max;
321
326
  p2_dmg.min = p2_max;
322
327
  continue;
323
328
  }
324
329
 
325
- try debug(writer, opts.chance.actions, false, .{ .color = i });
330
+ if (!summary) try debug(writer, opts.chance.actions, false, .{ .color = i });
326
331
 
327
332
  for (p1_dmg.min..@as(u9, p1_max) + 1) |p1d| {
328
333
  for (p2_dmg.min..@as(u9, p2_max) + 1) |p2d| {
@@ -346,17 +351,25 @@ pub fn transitions(
346
351
  return error.TestUnexpectedResult;
347
352
  }
348
353
  } else {
349
- try debug(writer, opts.chance.actions, false, .{ .dim = true });
354
+
350
355
 
351
356
  if (!matches(opts.chance.actions, i, frontier.items)) {
352
357
  try frontier.append(opts.chance.actions);
353
358
 
359
+ try debug(writer, opts.chance.actions, false, .{ .dim = true, .newline = false });
360
+ try writer.writeAll(" → ");
354
361
  try debug(writer, opts.chance.actions, true, .{
355
362
  .color = frontier.items.len - 1,
356
363
  .dim = true,
357
- .background = true
364
+ .background = true,
365
+ .indent = false,
358
366
  });
367
+ } else if (!summary) {
368
+ try debug(writer, opts.chance.actions, false, .{ .dim = true });
359
369
  }
370
+
371
+ // if (opts.chance.actions.p1.damage == 0) p1_max = 255;
372
+ // if (opts.chance.actions.p2.damage == 0) p2_max = 255;
360
373
  }
361
374
 
362
375
  p1_min = p1_max;
@@ -367,6 +380,12 @@ pub fn transitions(
367
380
  p1_dmg.min = p1_min;
368
381
 
369
382
  }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
383
+
384
+ if (@TypeOf(writer) != @TypeOf(std.io.null_writer)) {
385
+ p.reduce();
386
+ try writer.print(" = {s} ({d:.2}%)\n\n", .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))});
387
+ }
388
+
370
389
  }
371
390
 
372
391
  assert(frontier.items.len == i);
@@ -387,7 +406,86 @@ pub fn transitions(
387
406
  return stats;
388
407
  }
389
408
 
390
- inline fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
409
+ pub fn update(
410
+ battle: anytype,
411
+ c1: Choice,
412
+ c2: Choice,
413
+ options: anytype,
414
+ allocator: std.mem.Allocator,
415
+ writer: anytype,
416
+ transition: bool,
417
+ ) !Result {
418
+ options.chance.reset();
419
+
420
+ if (!pkmn.options.chance or !pkmn.options.calc or @TypeOf(battle.rng) != data.PRNG) {
421
+ return battle.update(c1, c2, options);
422
+ }
423
+
424
+ var copy = battle.*;
425
+ const actions = options.chance.actions;
426
+
427
+ // Perfom the actual update
428
+ const result = battle.update(c1, c2, options);
429
+
430
+ // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
431
+ var buf: [helpers.MAX_DIFFS]u8 = undefined;
432
+ var stream: protocol.ByteStream = .{ .buffer = &buf };
433
+ const n = try helpers.diff(&copy, battle, stream.writer());
434
+
435
+ // Applying the diff to the battle should take us back to the original copy
436
+ // of the battle (ignoring the RNG).
437
+ var patched = battle.*;
438
+ patched.rng = copy.rng;
439
+ helpers.patch(&patched, buf[0..n]);
440
+ try expectEqual(copy, patched);
441
+
442
+ if (!pkmn.options.chance or !pkmn.options.calc) return result;
443
+
444
+ if (transition) {
445
+ // Ensure we can generate all transitions from the same original state
446
+ // (we must change the battle's RNG from a FixedRNG to a PRNG because
447
+ // the transitions function relies on RNG for discovery of states)
448
+ if (try transitions(unfix(copy), c1, c2, allocator, writer, .{
449
+ .actions = actions,
450
+ .cap = true,
451
+ })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
452
+ }
453
+
454
+ // Demonstrate that we can produce the same state by forcing the RNG to behave the
455
+ // same as we observed - note that because we do not pass in a durations override
456
+ // mask none of the durations will be extended.
457
+ var override = pkmn.battle.options(
458
+ protocol.NULL,
459
+ chance.NULL,
460
+ Calc{ .overrides = .{ .actions = actions } },
461
+ );
462
+ const overridden = copy.update(c1, c2, &override);
463
+ try expectEqual(result, overridden);
464
+
465
+ // The actual battle excluding its RNG field should match a copy updated with
466
+ // overridden RNG (the copy RNG may have advanced because of no-ops)
467
+ copy.rng = battle.rng;
468
+ try expectEqual(copy, battle.*);
469
+
470
+ return result;
471
+ }
472
+
473
+ fn unfix(actual: anytype) data.Battle(data.PRNG) {
474
+ return .{
475
+ .sides = actual.sides,
476
+ .turn = actual.turn,
477
+ .last_damage = actual.last_damage,
478
+ .last_moves = actual.last_moves,
479
+ .rng = .{ .src = .{
480
+ .seed = if (pkmn.options.showdown)
481
+ 0x12345678
482
+ else
483
+ .{ 123, 234, 56, 78, 9, 101, 112, 131, 4 },
484
+ } },
485
+ };
486
+ }
487
+
488
+ fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
391
489
  for (frontier, 0..) |f, j| {
392
490
  // TODO: is skipping this redundant check worth it?
393
491
  if (i == j) continue;
@@ -408,19 +506,24 @@ const Style = struct {
408
506
  bold: bool = false,
409
507
  background: bool = false,
410
508
  dim: bool = false,
509
+ newline: bool = true,
510
+ indent: bool = true,
411
511
  };
412
512
 
413
513
  fn debug(writer: anytype, actions: Actions, shape: bool, style: Style) !void {
414
- _ = .{ writer, actions, shape, style };
415
- // const mod: usize = if (style.dim) 2 else 1;
416
- // const background: usize = if (style.background) 4 else 3;
417
- // const color: usize = if (style.color) |c| (c % 6) + 1 else 7;
418
- // if (style.dim or style.bold) try writer.print("\x1b[{d}m", .{mod});
419
- // try writer.print("\x1b[{d}{d}m", .{ background, color });
420
- // try actions.fmt(writer, shape);
421
- // try writer.writeAll("\x1b[0m\n");
422
- // DEBUG
423
- // if (style.dim) try writer.writeAll(" ");
424
- // try actions.fmt(writer, shape);
425
- // try writer.writeByte('\n');
514
+ if (style.indent) try writer.writeAll(" ");
515
+ if (tty) {
516
+ const mod: usize = if (style.dim) 2 else 1;
517
+ const background: usize = if (style.background) 4 else 3;
518
+ const color: usize = if (style.color) |c| (c % 6) + 1 else 7;
519
+
520
+ if (style.dim or style.bold) try writer.print("\x1b[{d}m", .{mod});
521
+ try writer.print("\x1b[{d}{d}m", .{ background, color });
522
+ try actions.fmt(writer, shape);
523
+ try writer.writeAll("\x1b[0m");
524
+ } else {
525
+ if (style.dim) try writer.writeAll(" ");
526
+ try actions.fmt(writer, shape);
527
+ }
528
+ if (style.newline) try writer.writeByte('\n');
426
529
  }