@pkmn/engine 0.1.0-dev.62399ed5 → 0.1.0-dev.6be9a775

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.
@@ -1,12 +1,61 @@
1
1
  const std = @import("std");
2
2
 
3
+ const expect = std.testing.expect;
3
4
  const expectEqual = std.testing.expectEqual;
4
5
 
6
+ const Pointer = if (@hasField(std.builtin.Type, "pointer")) .pointer else .Pointer;
7
+
5
8
  pub fn PointerType(comptime P: type, comptime C: type) type {
6
- return if (@typeInfo(P).Pointer.is_const) *const C else *C;
9
+ return if (@field(@typeInfo(P), @tagName(Pointer)).is_const) *const C else *C;
7
10
  }
8
11
 
9
12
  test PointerType {
10
13
  try expectEqual(*bool, PointerType(*u8, bool));
11
14
  try expectEqual(*const f64, PointerType(*const i32, f64));
12
15
  }
16
+
17
+ pub fn isPointerTo(p: anytype, comptime P: type) bool {
18
+ const info = @typeInfo(@TypeOf(p));
19
+ return switch (info) {
20
+ Pointer => @field(info, @tagName(Pointer)).child == P,
21
+ else => false,
22
+ };
23
+ }
24
+
25
+ test isPointerTo {
26
+ const S = struct {};
27
+ const s: S = .{};
28
+ try expect(!isPointerTo(s, S));
29
+ try expect(isPointerTo(&s, S));
30
+ }
31
+
32
+ // NOTE: std.mem.bytesAsValue backported from ziglang/zig#18061
33
+ pub fn bytesAsValue(comptime T: type, bytes: anytype) BytesAsValueReturnType(T, @TypeOf(bytes)) {
34
+ return @as(BytesAsValueReturnType(T, @TypeOf(bytes)), @ptrCast(bytes));
35
+ }
36
+
37
+ fn BytesAsValueReturnType(comptime T: type, comptime B: type) type {
38
+ return CopyPtrAttrs(B, .One, T);
39
+ }
40
+
41
+ fn CopyPtrAttrs(
42
+ comptime source: type,
43
+ comptime size: std.builtin.Type.Pointer.Size,
44
+ comptime child: type,
45
+ ) type {
46
+ const info = @field(@typeInfo(source), @tagName(Pointer));
47
+ const args = .{
48
+ .size = size,
49
+ .is_const = info.is_const,
50
+ .is_volatile = info.is_volatile,
51
+ .is_allowzero = info.is_allowzero,
52
+ .alignment = info.alignment,
53
+ .address_space = info.address_space,
54
+ .child = child,
55
+ .sentinel = null,
56
+ };
57
+ return @Type(if (@hasField(std.builtin.Type, "pointer"))
58
+ .{ .pointer = args }
59
+ else
60
+ .{ .Pointer = args });
61
+ }
@@ -17,6 +17,7 @@ const assert = std.debug.assert;
17
17
 
18
18
  const expect = std.testing.expect;
19
19
  const expectEqual = std.testing.expectEqual;
20
+ const expectEqualSlices = std.testing.expectEqualSlices;
20
21
 
21
22
  const enabled = pkmn.options.calc;
22
23
 
@@ -29,18 +30,19 @@ const Optional = optional.Optional;
29
30
  const Rational = rational.Rational;
30
31
 
31
32
  const PointerType = util.PointerType;
33
+ const isPointerTo = util.isPointerTo;
32
34
 
33
35
  const Actions = chance.Actions;
34
36
  const Action = chance.Action;
35
37
  const Chance = chance.Chance;
36
38
  const Durations = chance.Durations;
37
- const Duration = chance.Duration;
38
-
39
- const Rolls = helpers.Rolls;
40
39
 
41
40
  const tty = true; // DEBUG
42
41
  const summary = false; // DEBUG
43
42
 
43
+ const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
44
+ const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
45
+
44
46
  /// Information relevant to damage calculation that occured during a Generation I battle `update`.
45
47
  pub const Summaries = extern struct {
46
48
  /// Relevant information for Player 1.
@@ -54,7 +56,7 @@ pub const Summaries = extern struct {
54
56
 
55
57
  /// Returns the `Summary` for the given `player`.
56
58
  pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Summary) {
57
- assert(@typeInfo(@TypeOf(self)).Pointer.child == Summaries);
59
+ assert(isPointerTo(self, Summaries));
58
60
  return if (player == .P1) &self.p1 else &self.p2;
59
61
  }
60
62
  };
@@ -90,25 +92,11 @@ pub const Summary = extern struct {
90
92
  }
91
93
  };
92
94
 
93
- /// TODO
94
- pub const Overrides = extern struct {
95
- /// TODO
96
- actions: Actions = .{},
97
- /// TODO
98
- durations: Durations = .{},
99
-
100
- _: u32 = 0,
101
-
102
- comptime {
103
- assert(@sizeOf(Overrides) == 24);
104
- }
105
- };
106
-
107
95
  /// Allows for forcing the value of specific RNG events during a Generation I battle `update` via
108
96
  /// `overrides` and tracks `summaries` of information relevant to damage calculation.
109
97
  pub const Calc = struct {
110
98
  /// Overrides the normal behavior of the RNG during an `update` to force specific outcomes.
111
- overrides: Overrides = .{},
99
+ overrides: Actions = .{},
112
100
  /// Information relevant to damage calculation.
113
101
  summaries: Summaries = .{},
114
102
 
@@ -119,28 +107,14 @@ pub const Calc = struct {
119
107
  ) ?std.meta.FieldType(Action, field) {
120
108
  if (!enabled) return null;
121
109
 
122
- const overrides =
123
- if (player == .P1) self.overrides.actions.p1 else self.overrides.actions.p2;
124
- const val = @field(overrides, @tagName(field));
110
+ const val = @field(self.overrides.get(player), @tagName(field));
125
111
  return if (switch (@typeInfo(@TypeOf(val))) {
126
- .Enum => val != .None,
127
- .Int => val != 0,
112
+ Enum => val != .None,
113
+ Int => val != 0,
128
114
  else => unreachable,
129
115
  }) val else null;
130
116
  }
131
117
 
132
- pub fn modify(self: Calc, player: Player, comptime field: Duration.Field) ?bool {
133
- if (!enabled) return null;
134
-
135
- const durations = self.overrides.durations;
136
- const set = @field(if (player == .P1) durations.p1 else durations.p2, @tagName(field));
137
- if (set == 0) return null;
138
-
139
- const actions = self.overrides.actions;
140
- const overrides = if (player == .P1) actions.p1.durations else actions.p2.durations;
141
- return @field(overrides, @tagName(field)) != 0;
142
- }
143
-
144
118
  pub fn base(self: *Calc, player: Player, val: u16) void {
145
119
  if (!enabled) return;
146
120
 
@@ -174,11 +148,6 @@ const Null = struct {
174
148
  return null;
175
149
  }
176
150
 
177
- pub fn modify(self: Null, player: Player, comptime field: Duration.Field) ?bool {
178
- _ = .{ self, player, field };
179
- return null;
180
- }
181
-
182
151
  pub fn base(self: Null, player: Player, val: u16) void {
183
152
  _ = .{ self, player, val };
184
153
  }
@@ -200,7 +169,7 @@ pub const Stats = struct {
200
169
  };
201
170
 
202
171
  pub const Options = struct {
203
- actions: Actions = .{},
172
+ durations: Durations,
204
173
  seed: ?u64 = null,
205
174
  cap: bool = false,
206
175
  metronome: bool = false,
@@ -218,7 +187,6 @@ pub fn transitions(
218
187
  ) !?Stats {
219
188
  var stats: Stats = .{};
220
189
 
221
- const actions = options.actions;
222
190
  const cap = options.cap;
223
191
 
224
192
  var seen = std.AutoHashMap(Actions, void).init(allocator);
@@ -228,11 +196,8 @@ pub fn transitions(
228
196
 
229
197
  var opts = pkmn.battle.options(
230
198
  protocol.NULL,
231
- Chance(Rational(u128)){ .probability = .{}, .actions = actions },
232
- Calc{ .overrides = .{ .actions = actions, .durations = .{
233
- .p1 = actions.p1.durations,
234
- .p2 = actions.p2.durations,
235
- } } },
199
+ Chance(Rational(u128)){ .probability = .{}, .durations = options.durations },
200
+ Calc{},
236
201
  );
237
202
 
238
203
  var b = battle;
@@ -254,13 +219,9 @@ pub fn transitions(
254
219
  var i: usize = 0;
255
220
  assert(frontier.items.len == 1);
256
221
  while (i < frontier.items.len) : (i += 1) {
257
- const template = frontier.items[i];
258
- opts.calc.overrides.durations = .{
259
- .p1 = template.p1.durations,
260
- .p2 = template.p2.durations,
261
- };
222
+ const f = frontier.items[i];
262
223
 
263
- try debug(writer, template, .{
224
+ try debug(writer, f, .{
264
225
  .shape = true,
265
226
  .color = i,
266
227
  .bold = true,
@@ -268,43 +229,34 @@ pub fn transitions(
268
229
  .indent = false,
269
230
  });
270
231
 
271
- var a: Actions = .{ .p1 = .{ .metronome = p1_move }, .p2 = .{ .metronome = p2_move } };
272
-
273
- for (Rolls.speedTie(template.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
274
- for (Rolls.sleep(template.p1, actions.p1)) |p1_slp| { a.p1.durations.sleep = p1_slp;
275
- for (Rolls.sleep(template.p2, actions.p2)) |p2_slp| { a.p2.durations.sleep = p2_slp;
276
- for (Rolls.disable(template.p1, actions.p1, p1_slp)) |p1_disable| { a.p1.durations.disable = p1_disable;
277
- for (Rolls.disable(template.p2, actions.p2, p2_slp)) |p2_disable| { a.p2.durations.disable = p2_disable;
278
- for (Rolls.confusion(template.p1, actions.p1, p1_slp)) |p1_cfz| { a.p1.durations.confusion = p1_cfz;
279
- for (Rolls.confusion(template.p2, actions.p2, p1_slp)) |p2_cfz| { a.p2.durations.confusion = p2_cfz;
280
- for (Rolls.confused(template.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
281
- for (Rolls.confused(template.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
282
- for (Rolls.paralyzed(template.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
283
- for (Rolls.paralyzed(template.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
284
- for (Rolls.attacking(template.p1, actions.p1, p1_par)) |p1_atk| { a.p1.durations.attacking = p1_atk;
285
- for (Rolls.attacking(template.p2, actions.p2, p2_par)) |p2_atk| { a.p2.durations.attacking = p2_atk;
286
- for (Rolls.binding(template.p1, actions.p1, p1_par)) |p1_bind| { a.p1.durations.binding = p1_bind;
287
- for (Rolls.binding(template.p2, actions.p2, p2_par)) |p2_bind| { a.p2.durations.binding = p2_bind;
288
- for (Rolls.duration(template.p1, p1_par)) |p1_dur| { a.p1.duration = p1_dur;
289
- for (Rolls.duration(template.p2, p2_par)) |p2_dur| { a.p2.duration = p2_dur;
290
- for (Rolls.hit(template.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
291
- for (Rolls.hit(template.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
292
- for (Rolls.psywave(template.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
293
- for (Rolls.psywave(template.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
294
- for (Rolls.moveSlot(template.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
295
- for (Rolls.moveSlot(template.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
296
- for (Rolls.multiHit(template.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
297
- for (Rolls.multiHit(template.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
298
- for (Rolls.secondaryChance(template.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
299
- for (Rolls.secondaryChance(template.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
300
- for (Rolls.criticalHit(template.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
301
- for (Rolls.criticalHit(template.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
302
-
303
- var p1_dmg = Rolls.damage(template.p1, p1_hit);
232
+ var a: Actions = .{
233
+ .p1 = .{ .metronome = p1_move, .pp = f.p1.pp, .duration = f.p1.duration },
234
+ .p2 = .{ .metronome = p2_move, .pp = f.p2.pp, .duration = f.p2.duration },
235
+ };
236
+
237
+ for (Rolls.speedTie(f.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
238
+ for (Rolls.confused(f.p1)) |p1_cfzd| { a.p1.confused = p1_cfzd;
239
+ for (Rolls.confused(f.p2)) |p2_cfzd| { a.p2.confused = p2_cfzd;
240
+ for (Rolls.paralyzed(f.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
241
+ for (Rolls.paralyzed(f.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
242
+ for (Rolls.hit(f.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
243
+ for (Rolls.hit(f.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
244
+ for (Rolls.psywave(f.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
245
+ for (Rolls.psywave(f.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
246
+ for (Rolls.moveSlot(f.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
247
+ for (Rolls.moveSlot(f.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
248
+ for (Rolls.multiHit(f.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
249
+ for (Rolls.multiHit(f.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
250
+ for (Rolls.secondaryChance(f.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
251
+ for (Rolls.secondaryChance(f.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
252
+ for (Rolls.criticalHit(f.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
253
+ for (Rolls.criticalHit(f.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
254
+
255
+ var p1_dmg = Rolls.damage(f.p1, p1_hit);
304
256
  while (p1_dmg.min < p1_dmg.max) : (p1_dmg.min += 1) {
305
257
  a.p1.damage = @intCast(p1_dmg.min);
306
258
 
307
- var p2_dmg = Rolls.damage(template.p2, p2_hit);
259
+ var p2_dmg = Rolls.damage(f.p2, p2_hit);
308
260
 
309
261
  const p1_min: u9 = p1_dmg.min;
310
262
  const p2_min: u9 = p2_dmg.min;
@@ -312,13 +264,14 @@ pub fn transitions(
312
264
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
313
265
  a.p2.damage = @intCast(p2_dmg.min);
314
266
 
315
- opts.calc.overrides.actions = a;
267
+ opts.calc.overrides = a;
316
268
  opts.calc.summaries = .{};
317
- opts.chance = .{ .probability = .{}, .actions = actions };
269
+ opts.chance = .{ .probability = .{}, .durations = options.durations };
318
270
  const q = &opts.chance.probability;
319
271
 
320
272
  b = battle;
321
273
  _ = try b.update(c1, c2, &opts);
274
+
322
275
  stats.updates += 1;
323
276
 
324
277
  const summaries = &opts.calc.summaries;
@@ -329,7 +282,7 @@ pub fn transitions(
329
282
  const p2_max: u9 =
330
283
  try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
331
284
 
332
- if (opts.chance.actions.matches(template)) {
285
+ if (opts.chance.actions.matches(f)) {
333
286
  if (!opts.chance.actions.eql(a)) {
334
287
  if (!summary) {
335
288
  try debug(writer, opts.chance.actions, .{
@@ -405,7 +358,7 @@ pub fn transitions(
405
358
  p2_dmg.min = p2_max;
406
359
  }
407
360
 
408
- }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
361
+ }}}}}}}}}}}}}}}}}}
409
362
 
410
363
  if (@TypeOf(writer) != @TypeOf(std.io.null_writer)) {
411
364
  p.reduce();
@@ -444,61 +397,90 @@ pub fn update(
444
397
  writer: anytype,
445
398
  transition: bool,
446
399
  ) !Result {
400
+ const durations = options.chance.durations;
447
401
  options.chance.reset();
448
402
 
449
- if (!pkmn.options.chance or !pkmn.options.calc or @TypeOf(battle.rng) != data.PRNG) {
450
- return battle.update(c1, c2, options);
451
- }
403
+ if (!pkmn.options.chance or !pkmn.options.calc) return battle.update(c1, c2, options);
452
404
 
453
- var copy = battle.*;
454
- const actions = options.chance.actions;
405
+ const original = battle.*;
455
406
 
456
407
  // Perfom the actual update
457
408
  const result = battle.update(c1, c2, options);
458
-
459
- // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
460
- var buf: [helpers.MAX_DIFFS]u8 = undefined;
461
- var stream: protocol.ByteStream = .{ .buffer = &buf };
462
- const n = try helpers.diff(&copy, battle, stream.writer());
463
-
464
- // Applying the diff to the battle should take us back to the original copy
465
- // of the battle (ignoring the RNG).
466
- var patched = battle.*;
467
- patched.rng = copy.rng;
468
- helpers.patch(&patched, buf[0..n]);
469
- try expectEqual(copy, patched);
470
-
471
- if (!pkmn.options.chance or !pkmn.options.calc) return result;
409
+ var overrides = options.chance.actions;
410
+ const updated = options.chance.durations;
411
+
412
+ if (@TypeOf(battle.rng) == data.PRNG) {
413
+ // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
414
+ var buf: [helpers.MAX_DIFFS]u8 = undefined;
415
+ var stream: protocol.ByteStream = .{ .buffer = &buf };
416
+ const n = try helpers.diff(&original, battle, stream.writer());
417
+
418
+ // Applying the diff to the battle should take us back to the original copy
419
+ // of the battle (ignoring the RNG).
420
+ var patched = battle.*;
421
+ patched.rng = original.rng;
422
+ helpers.patch(&patched, buf[0..n]);
423
+ try expectEqual(original, patched);
424
+ }
472
425
 
473
426
  if (transition) {
474
427
  // Ensure we can generate all transitions from the same original state
475
428
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
476
429
  // the transitions function relies on RNG for discovery of states)
477
- if (try transitions(unfix(copy), c1, c2, allocator, writer, .{
478
- .actions = actions,
430
+ if (try transitions(unfix(original), c1, c2, allocator, writer, .{
431
+ .durations = durations,
479
432
  .cap = true,
480
433
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
481
434
  }
482
435
 
483
436
  // Demonstrate that we can produce the same state by forcing the RNG to behave the
484
- // same as we observed - note that because we do not pass in a durations override
485
- // mask none of the durations will be extended.
437
+ // same as we observed.
486
438
  var override = pkmn.battle.options(
487
439
  protocol.NULL,
488
- chance.NULL,
489
- Calc{ .overrides = .{ .actions = actions } },
440
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
441
+ Calc{ .overrides = overrides },
490
442
  );
491
- const overridden = copy.update(c1, c2, &override);
443
+ var copy = original;
444
+ var overridden = copy.update(c1, c2, &override);
492
445
  try expectEqual(result, overridden);
446
+ try expectEqualActions(overrides, override.chance.actions);
447
+ try expectEqual(updated, override.chance.durations);
493
448
 
494
449
  // The actual battle excluding its RNG field should match a copy updated with
495
450
  // overridden RNG (the copy RNG may have advanced because of no-ops)
496
451
  copy.rng = battle.rng;
497
- try expectEqual(copy, battle.*);
452
+ try expectEqual(battle.*, copy);
453
+
454
+ // Demonstrate that even with the duration rolls changed, the duration
455
+ // fields on their own are sufficient to achieve the same outcomes
456
+ // NB: The actual battle bytes will differ because the durations won't match
457
+ overrides.p1.duration = if (overrides.p1.duration > 0) 2 else 0;
458
+ overrides.p2.duration = if (overrides.p2.duration > 0) 2 else 0;
459
+
460
+ override = pkmn.battle.options(
461
+ protocol.NULL,
462
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
463
+ Calc{ .overrides = overrides },
464
+ );
465
+ copy = original;
466
+ overridden = copy.update(c1, c2, &override);
467
+ try expectEqual(result, overridden);
468
+ try expectEqualActions(overrides, override.chance.actions);
469
+ try expectEqual(updated, override.chance.durations);
498
470
 
499
471
  return result;
500
472
  }
501
473
 
474
+ fn expectEqualActions(expected: Actions, actual: Actions) !void {
475
+ return expectEqual(expected, actual) catch |e| switch (e) {
476
+ error.TestExpectedEqual => {
477
+ std.debug.print("expected {}, found {}\n", .{ expected, actual });
478
+ return e;
479
+ },
480
+ else => return e,
481
+ };
482
+ }
483
+
502
484
  fn unfix(actual: anytype) data.Battle(data.PRNG) {
503
485
  return .{
504
486
  .sides = actual.sides,
@@ -594,3 +576,231 @@ fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
594
576
 
595
577
  try writer.writeAll(output[0..len]);
596
578
  }
579
+
580
+ const NONE = @intFromEnum(Optional(bool).None);
581
+
582
+ /// Helper functions that efficiently return valid ranges for various RNG events based on the
583
+ /// state of an `Action` and other events to be used to construct a "transitions" function.
584
+ pub const Rolls = struct {
585
+ const PLAYER_NONE = [_]Optional(Player){.None};
586
+ const PLAYERS = [_]Optional(Player){ .P1, .P2 };
587
+
588
+ /// Returns a slice with the correct range of values for speed ties given the `action` state.
589
+ pub fn speedTie(action: Action) []const Optional(Player) {
590
+ return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
591
+ }
592
+
593
+ const BOOL_NONE = [_]Optional(bool){.None};
594
+ const BOOLS = [_]Optional(bool){ .false, .true };
595
+
596
+ /// Returns a slice with the correct range of values for hits given the `action` state
597
+ /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
598
+ pub fn hit(action: Action, parent: Optional(bool)) []const Optional(bool) {
599
+ if (parent == .true) return &BOOL_NONE;
600
+ return if (action.hit == .None) &BOOL_NONE else &BOOLS;
601
+ }
602
+
603
+ /// Returns a slice with the correct range of values for critical hits given the `action` state
604
+ /// and the state of the `parent` (whether the player's Pokémon's move hit).
605
+ pub fn criticalHit(action: Action, parent: Optional(bool)) []const Optional(bool) {
606
+ if (parent == .false) return &BOOL_NONE;
607
+ return if (action.critical_hit == .None) &BOOL_NONE else &BOOLS;
608
+ }
609
+
610
+ /// Returns a slice with the correct range of values for secondary chances hits given the
611
+ /// `action` state and the state of the `parent` (whether the player's Pokémon's move hit).
612
+ pub fn secondaryChance(action: Action, parent: Optional(bool)) []const Optional(bool) {
613
+ if (parent == .false) return &BOOL_NONE;
614
+ return if (@field(action, "secondary_chance") == .None) &BOOL_NONE else &BOOLS;
615
+ }
616
+
617
+ /// The min and max bounds on iteration over damage rolls.
618
+ pub const Range = struct { min: u9, max: u9 };
619
+
620
+ /// Returns the range bounding damage rolls given the `action` state and the state of
621
+ /// the `parent` (whether the player's Pokémon's move hit).
622
+ pub fn damage(action: Action, parent: Optional(bool)) Range {
623
+ return if (parent == .false or action.damage == 0)
624
+ .{ .min = 0, .max = 1 }
625
+ else
626
+ .{ .min = 217, .max = 256 };
627
+ }
628
+
629
+ /// Returns the max damage roll which will produce the same damage as `roll`
630
+ /// given the base damage in `summaries`.
631
+ pub fn coalesce(player: Player, roll: u8, summaries: *Summaries, cap: bool) !u8 {
632
+ if (roll == 0) return roll;
633
+
634
+ const dmg = summaries.get(player.foe()).damage;
635
+ if (dmg.base == 0 or (cap and dmg.capped)) return 255;
636
+
637
+ // Closed form solution for max damage roll provided by Orion Taylor (taylorott)
638
+ return @min(255, roll + ((254 - ((@as(u32, dmg.base) * roll) % 255)) / dmg.base));
639
+ }
640
+
641
+ /// Returns a slice with the correct range of values for confused.
642
+ pub fn confused(action: Action) []const Optional(bool) {
643
+ return if (action.confused == .None) &BOOL_NONE else &BOOLS;
644
+ }
645
+
646
+ /// Returns a slice with the correct range of values for paralysis given the `action` state
647
+ /// and the state of the `parent` (whether the player's Pokémon was confused).
648
+ pub fn paralyzed(action: Action, parent: Optional(bool)) []const Optional(bool) {
649
+ if (parent == .true) return &BOOL_NONE;
650
+ return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
651
+ }
652
+
653
+ const SLOT_NONE = [_]u4{0};
654
+ const SLOT = [_]u4{ 1, 2, 3, 4 };
655
+
656
+ /// Returns a slice with a range of values for move slots given the `action` state
657
+ /// and the state of the `parent` (whether the player's Pokémon's move hit).
658
+ ///
659
+ /// These slots may or **may not be valid** as slots may be unset / have 0 PP.
660
+ pub fn moveSlot(action: Action, parent: Optional(bool)) []const u4 {
661
+ if (parent == .false) return &SLOT_NONE;
662
+ return if (action.move_slot == 0) &SLOT_NONE else &SLOT;
663
+ }
664
+
665
+ const MULTI_NONE = [_]u4{0};
666
+ const MULTI = [_]u4{ 2, 3, 4, 5 };
667
+
668
+ /// Returns a slice with the correct range of values for multi hit given the `action` state
669
+ /// and the state of the `parent` (whether the player's Pokémon's move hit).
670
+ pub fn multiHit(action: Action, parent: Optional(bool)) []const u4 {
671
+ if (parent == .false) return &MULTI_NONE;
672
+ return if (action.multi_hit == 0) &MULTI_NONE else &MULTI;
673
+ }
674
+
675
+ const PSYWAVE_NONE = [_]u8{0};
676
+ const PSYWAVE = init: {
677
+ var rolls: [150]u8 = undefined;
678
+ for (0..150) |i| rolls[i] = i + 1;
679
+ break :init rolls;
680
+ };
681
+
682
+ /// Returns a slice with the correct range of values for psywave given the `action` state,
683
+ /// the `side`, and the state of the `parent` (whether the player's Pokémon's move hit).
684
+ pub fn psywave(
685
+ action: Action,
686
+ side: *data.Side,
687
+ parent: Optional(bool),
688
+ ) []const u8 {
689
+ if (parent == .false) return &PSYWAVE_NONE;
690
+ return if (action.psywave == 0)
691
+ &PSYWAVE_NONE
692
+ else
693
+ PSYWAVE[0 .. @as(u16, side.stored().level) * 3 / 2];
694
+ }
695
+
696
+ const MOVE_NONE = [_]data.Move{.None};
697
+
698
+ /// Returns a slice with the correct range of values for metronome given the `action` state.
699
+ pub fn metronome(action: Action) []const data.Move {
700
+ return if (action.metronome == .None) &MOVE_NONE else &data.Move.METRONOME;
701
+ }
702
+ };
703
+
704
+ test "Rolls.speedTie" {
705
+ const actions: Actions = .{ .p1 = .{ .speed_tie = .P2 } };
706
+ try expectEqualSlices(Optional(Player), &.{ .P1, .P2 }, Rolls.speedTie(actions.p1));
707
+ try expectEqualSlices(Optional(Player), &.{.None}, Rolls.speedTie(actions.p2));
708
+ }
709
+
710
+ test "Rolls.damage" {
711
+ const actions: Actions = .{ .p2 = .{ .damage = 221 } };
712
+ try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p1, .None));
713
+ try expectEqual(Rolls.Range{ .min = 217, .max = 256 }, Rolls.damage(actions.p2, .None));
714
+ try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p2, .false));
715
+ }
716
+
717
+ test "Rolls.coalesce" {
718
+ var summaries = Summaries{ .p1 = .{ .damage = .{ .base = 74, .final = 69, .capped = true } } };
719
+ try expectEqual(@as(u8, 0), try Rolls.coalesce(.P2, 0, &summaries, false));
720
+ try expectEqual(@as(u8, 241), try Rolls.coalesce(.P2, 238, &summaries, false));
721
+ try expectEqual(@as(u8, 255), try Rolls.coalesce(.P2, 238, &summaries, true));
722
+ summaries.p1.damage.final = 74;
723
+ try expectEqual(@as(u8, 217), try Rolls.coalesce(.P2, 217, &summaries, false));
724
+ try expectEqual(@as(u8, 255), try Rolls.coalesce(.P2, 217, &summaries, true));
725
+ }
726
+
727
+ test "Rolls.hit" {
728
+ const actions: Actions = .{ .p2 = .{ .hit = .true } };
729
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.hit(actions.p1, .None));
730
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.hit(actions.p2, .None));
731
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.hit(actions.p2, .true));
732
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.hit(actions.p2, .false));
733
+ }
734
+
735
+ test "Rolls.secondaryChance" {
736
+ const actions: Actions = .{ .p1 = .{ .secondary_chance = .true } };
737
+ try expectEqualSlices(
738
+ Optional(bool),
739
+ &.{ .false, .true },
740
+ Rolls.secondaryChance(actions.p1, .None),
741
+ );
742
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.secondaryChance(actions.p1, .false));
743
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.secondaryChance(actions.p2, .None));
744
+ }
745
+
746
+ test "Rolls.criticalHit" {
747
+ const actions: Actions = .{ .p1 = .{ .critical_hit = .true } };
748
+ try expectEqualSlices(
749
+ Optional(bool),
750
+ &.{ .false, .true },
751
+ Rolls.criticalHit(actions.p1, .None),
752
+ );
753
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p1, .false));
754
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p2, .None));
755
+ }
756
+
757
+ test "Rolls.confused" {
758
+ const actions: Actions = .{ .p2 = .{ .confused = .true } };
759
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p1));
760
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.confused(actions.p2));
761
+ }
762
+
763
+ test "Rolls.paralyzed" {
764
+ const actions: Actions = .{ .p2 = .{ .paralyzed = .true } };
765
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.paralyzed(actions.p1, .None));
766
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.paralyzed(actions.p2, .None));
767
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.paralyzed(actions.p2, .false));
768
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.paralyzed(actions.p2, .true));
769
+ }
770
+
771
+ test "Rolls.moveSlot" {
772
+ const actions: Actions = .{ .p2 = .{ .move_slot = 3 } };
773
+ try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p1, .None));
774
+ try expectEqualSlices(u4, &.{ 1, 2, 3, 4 }, Rolls.moveSlot(actions.p2, .None));
775
+ try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p2, .false));
776
+ }
777
+
778
+ test "Rolls.multiHit" {
779
+ const actions: Actions = .{ .p2 = .{ .multi_hit = 3 } };
780
+ try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p1, .None));
781
+ try expectEqualSlices(u4, &.{ 2, 3, 4, 5 }, Rolls.multiHit(actions.p2, .None));
782
+ try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p2, .false));
783
+ }
784
+
785
+ test "Rolls.metronome" {
786
+ const actions: Actions = .{ .p2 = .{ .metronome = .Surf } };
787
+ try expectEqualSlices(data.Move, &.{.None}, Rolls.metronome(actions.p1));
788
+ try expectEqual(@as(data.Move, @enumFromInt(24)), Rolls.metronome(actions.p2)[23]);
789
+ }
790
+
791
+ test "Rolls.psywave" {
792
+ const actions: Actions = .{ .p2 = .{ .psywave = 79 } };
793
+ var side = helpers.Side.init(&[_]helpers.Pokemon{.{
794
+ .species = .Bulbasaur,
795
+ .level = 100,
796
+ .moves = &[_]data.Move{.Tackle},
797
+ }});
798
+
799
+ try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p1, &side, .None));
800
+ var rolls = Rolls.psywave(actions.p2, &side, .None);
801
+ try expectEqual(@as(u8, 150), rolls[rolls.len - 1]);
802
+ side.stored().level = 81;
803
+ rolls = Rolls.psywave(actions.p2, &side, .None);
804
+ try expectEqual(@as(u8, 121), rolls[rolls.len - 1]);
805
+ try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p2, &side, .false));
806
+ }