@pkmn/engine 0.1.0-dev.028ab165

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 (62) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +343 -0
  3. package/build/pkg/addon.d.ts +7 -0
  4. package/build/pkg/addon.js +112 -0
  5. package/build/pkg/addon.js.map +1 -0
  6. package/build/pkg/data/ids.json +240 -0
  7. package/build/pkg/data/layout.json +108 -0
  8. package/build/pkg/data/protocol.json +230 -0
  9. package/build/pkg/data.d.ts +91 -0
  10. package/build/pkg/data.js +150 -0
  11. package/build/pkg/data.js.map +1 -0
  12. package/build/pkg/gen1.d.ts +110 -0
  13. package/build/pkg/gen1.js +684 -0
  14. package/build/pkg/gen1.js.map +1 -0
  15. package/build/pkg/index.d.ts +386 -0
  16. package/build/pkg/index.js +122 -0
  17. package/build/pkg/index.js.map +1 -0
  18. package/build/pkg/protocol.d.ts +73 -0
  19. package/build/pkg/protocol.js +473 -0
  20. package/build/pkg/protocol.js.map +1 -0
  21. package/build/test/display.js +400 -0
  22. package/build/test/display.js.map +1 -0
  23. package/build/tools/debug.js +135 -0
  24. package/build/tools/debug.js.map +1 -0
  25. package/build.zig +371 -0
  26. package/package.json +224 -0
  27. package/src/bin/install-pkmn-engine +396 -0
  28. package/src/bin/pkmn-debug +62 -0
  29. package/src/lib/binding/node.zig +134 -0
  30. package/src/lib/binding/wasm.zig +12 -0
  31. package/src/lib/common/battle.zig +17 -0
  32. package/src/lib/common/data.zig +117 -0
  33. package/src/lib/common/debug.zig +74 -0
  34. package/src/lib/common/js.zig +118 -0
  35. package/src/lib/common/optional.zig +67 -0
  36. package/src/lib/common/options.zig +49 -0
  37. package/src/lib/common/protocol.zig +1841 -0
  38. package/src/lib/common/rational.zig +353 -0
  39. package/src/lib/common/rng.zig +243 -0
  40. package/src/lib/common/util.zig +12 -0
  41. package/src/lib/gen1/README.md +894 -0
  42. package/src/lib/gen1/calc.zig +426 -0
  43. package/src/lib/gen1/chance.zig +1060 -0
  44. package/src/lib/gen1/data/moves.zig +1866 -0
  45. package/src/lib/gen1/data/species.zig +1105 -0
  46. package/src/lib/gen1/data/types.zig +131 -0
  47. package/src/lib/gen1/data.zig +634 -0
  48. package/src/lib/gen1/helpers.zig +779 -0
  49. package/src/lib/gen1/mechanics.zig +3128 -0
  50. package/src/lib/gen2/README.md +329 -0
  51. package/src/lib/gen2/calc.zig +143 -0
  52. package/src/lib/gen2/chance.zig +1314 -0
  53. package/src/lib/gen2/data/items.zig +230 -0
  54. package/src/lib/gen2/data/moves.zig +2878 -0
  55. package/src/lib/gen2/data/species.zig +1798 -0
  56. package/src/lib/gen2/data/types.zig +226 -0
  57. package/src/lib/gen2/data.zig +463 -0
  58. package/src/lib/gen2/generated.zig +808 -0
  59. package/src/lib/gen2/helpers.zig +219 -0
  60. package/src/lib/gen2/mechanics.zig +3551 -0
  61. package/src/lib/pkmn.zig +113 -0
  62. package/src/test/display/pkmn.html.tmpl +368 -0
@@ -0,0 +1,634 @@
1
+ const std = @import("std");
2
+ const builtin = @import("builtin");
3
+
4
+ const pkmn = @import("../pkmn.zig");
5
+
6
+ const data = @import("../common/data.zig");
7
+ const protocol = @import("../common/protocol.zig");
8
+ const rng = @import("../common/rng.zig");
9
+ const util = @import("../common/util.zig");
10
+
11
+ const moves = @import("data/moves.zig");
12
+ const species = @import("data/species.zig");
13
+ const types = @import("data/types.zig");
14
+
15
+ const calc = @import("calc.zig");
16
+ const chance = @import("chance.zig");
17
+ const mechanics = @import("mechanics.zig");
18
+
19
+ const assert = std.debug.assert;
20
+ const expectEqual = std.testing.expectEqual;
21
+ const expect = std.testing.expect;
22
+
23
+ /// The minimum size in bytes required to hold all Generation I choice options.
24
+ pub const MAX_CHOICES: usize = 9; // move 1..4, switch 2..6
25
+ /// The optimal size in bytes required to hold all Generation I choice options.
26
+ /// At least as large as MAX_CHOICES.
27
+ pub const MAX_LOGS: usize = 180;
28
+
29
+ /// The maximum number of bytes possibly logged by a single Generation I update.
30
+ pub const CHOICES_SIZE = if (builtin.mode == .ReleaseSmall)
31
+ MAX_CHOICES
32
+ else
33
+ std.math.ceilPowerOfTwo(usize, MAX_CHOICES) catch unreachable;
34
+ /// The optimal size in bytes required to hold the largest amount of log data possible from a
35
+ /// single Generation I update. At least as large as MAX_LOGS.
36
+ pub const LOGS_SIZE = if (builtin.mode == .ReleaseSmall)
37
+ MAX_LOGS
38
+ else
39
+ std.math.ceilPowerOfTwo(usize, MAX_LOGS) catch unreachable;
40
+
41
+ /// Null object pattern implementation of pkmn.battle.Options for Generation I.
42
+ pub const NULL = pkmn.battle.options(protocol.NULL, chance.NULL, calc.NULL);
43
+
44
+ const Choice = data.Choice;
45
+ const ID = data.ID;
46
+ const Player = data.Player;
47
+ const Result = data.Result;
48
+
49
+ const PointerType = util.PointerType;
50
+
51
+ const showdown = pkmn.options.showdown;
52
+
53
+ /// The pseudo random number generator used by Generation I.
54
+ pub const PRNG = rng.PRNG(1);
55
+
56
+ /// Representation of a Generation I battle.
57
+ pub fn Battle(comptime RNG: anytype) type {
58
+ return extern struct {
59
+ const Self = @This();
60
+
61
+ /// The sides involved in the battle.
62
+ sides: [2]Side,
63
+ /// The battle's current turn number.
64
+ turn: u16 = 0,
65
+ /// The last damage dealt by either side.
66
+ last_damage: u16 = 0,
67
+ /// Details about the last move selected/used by each player required to detect desyncs.
68
+ last_moves: [2]MoveDetails = [_]MoveDetails{.{}} ** 2,
69
+ /// The pseudo random number generator.
70
+ rng: RNG,
71
+
72
+ /// Returns the `Side` for the given `player`.
73
+ pub fn side(self: anytype, player: Player) PointerType(@TypeOf(self), Side) {
74
+ assert(@typeInfo(@TypeOf(self)).Pointer.child == Self);
75
+ return &self.sides[@intFromEnum(player)];
76
+ }
77
+
78
+ /// Returns the `Side` of the opponent for the given `player`
79
+ pub fn foe(self: anytype, player: Player) PointerType(@TypeOf(self), Side) {
80
+ assert(@typeInfo(@TypeOf(self)).Pointer.child == Self);
81
+ return &self.sides[@intFromEnum(player.foe())];
82
+ }
83
+
84
+ /// Returns an identifier for the active Pokémon of `player`.
85
+ pub fn active(self: *const Self, player: Player) ID {
86
+ return player.ident(@intCast(self.side(player).order[0]));
87
+ }
88
+
89
+ /// Returns the `MoveDetails` for the given `player`.
90
+ pub fn lastMove(
91
+ self: anytype,
92
+ player: Player,
93
+ ) PointerType(@TypeOf(self), MoveDetails) {
94
+ assert(@typeInfo(@TypeOf(self)).Pointer.child == Self);
95
+ return &self.last_moves[@intFromEnum(player)];
96
+ }
97
+
98
+ /// Returns the result of applying Player 1's choice `c1` and Player 2's choice `c2` to the
99
+ /// battle, optionally writing protocol message logs to `options.log` if `pkmn.options.log`
100
+ /// is enabled.
101
+ pub fn update(self: *Self, c1: Choice, c2: Choice, options: anytype) !Result {
102
+ return mechanics.update(self, c1, c2, options);
103
+ }
104
+
105
+ /// Fills in at most `out.len` possible choices for the `player` given the previous `result`
106
+ /// of an `update` and Generation I battle state and returns the number of choices
107
+ /// available. Note that reading values in `out` which occur at indexes > the return value
108
+ /// of this function could result in reading potentially garbage data.
109
+ ///
110
+ /// This function may return 0 due to how the Transform + Mirror Move/Metronome PP error
111
+ /// interacts with Disable, in which case there are no possible choices for the player to
112
+ /// make (i.e. on the cartridge a soft-lock occurs).
113
+ ///
114
+ /// This function will always return choices > 0 if `pkmn.options.showdown` is true.
115
+ pub fn choices(self: *Self, player: Player, request: Choice.Type, out: []Choice) u8 {
116
+ return mechanics.choices(self, player, request, out);
117
+ }
118
+ };
119
+ }
120
+
121
+ test Battle {
122
+ try expectEqual(384, @sizeOf(Battle(PRNG)));
123
+ }
124
+
125
+ /// Representation of one side of a Generation I Pokémon battle.
126
+ pub const Side = extern struct {
127
+ /// The player's party in its original order
128
+ pokemon: [6]Pokemon = [_]Pokemon{.{}} ** 6,
129
+ /// The active Pokémon for the side, zero initialized if the battle has yet to start.
130
+ /// Note that fainted Pokémon are still consider "active" until their replacement switches in.
131
+ active: ActivePokemon = .{},
132
+ /// One-based slot indexes reflecting the current order of the player's party.
133
+ order: [6]u8 = [_]u8{0} ** 6,
134
+ /// The last move the player selected.
135
+ last_selected_move: Move = .None,
136
+ /// The last move the player used.
137
+ last_used_move: Move = .None,
138
+
139
+ comptime {
140
+ assert(@sizeOf(Side) == 184);
141
+ }
142
+
143
+ /// Returns the stored `Pokemon` corresponding to the one-indexed party `slot`.
144
+ pub fn get(self: anytype, slot: u8) PointerType(@TypeOf(self), Pokemon) {
145
+ assert(@typeInfo(@TypeOf(self)).Pointer.child == Side);
146
+ assert(slot > 0 and slot <= 6);
147
+ const id = self.order[slot - 1];
148
+ assert(id > 0 and id <= 6);
149
+ return &self.pokemon[id - 1];
150
+ }
151
+
152
+ /// Returns the stored `Pokemon` corresponding to the `active` Pokémon (slot 1).
153
+ pub fn stored(self: anytype) PointerType(@TypeOf(self), Pokemon) {
154
+ assert(@typeInfo(@TypeOf(self)).Pointer.child == Side);
155
+ return self.get(1);
156
+ }
157
+ };
158
+
159
+ /// Representation of the state for single Generation I Pokémon while active in battle.
160
+ pub const ActivePokemon = extern struct {
161
+ /// The active Pokémon's modified stats.
162
+ stats: Stats(u16) = .{},
163
+ /// The active Pokémon's current species.
164
+ species: Species = .None,
165
+ /// The active Pokémon's current types.
166
+ types: Types = .{},
167
+ /// The active Pokémon's boosts.
168
+ boosts: Boosts = .{},
169
+ /// The active Pokémon's volatile statuses and associated data.
170
+ volatiles: Volatiles = .{},
171
+ /// The active Pokémon's current move slots.
172
+ moves: [4]MoveSlot = [_]MoveSlot{.{}} ** 4,
173
+
174
+ comptime {
175
+ assert(@sizeOf(ActivePokemon) == 32);
176
+ }
177
+
178
+ /// Returns the active Pokémon's current move slot located at the one-indexed `mslot`.
179
+ pub fn move(self: anytype, mslot: u8) PointerType(@TypeOf(self), MoveSlot) {
180
+ assert(@typeInfo(@TypeOf(self)).Pointer.child == ActivePokemon);
181
+ assert(mslot > 0 and mslot <= 4);
182
+ assert(self.moves[mslot - 1].id != .None);
183
+ return &self.moves[mslot - 1];
184
+ }
185
+ };
186
+
187
+ /// Representation of the state for single Generation I Pokémon while inactive in the party.
188
+ pub const Pokemon = extern struct {
189
+ /// The Pokémon's unmodified stats.
190
+ stats: Stats(u16) = .{},
191
+ /// The Pokémon's original stored move slots.
192
+ moves: [4]MoveSlot = [_]MoveSlot{.{}} ** 4,
193
+ /// The Pokémon's current HP.
194
+ hp: u16 = 0,
195
+ /// The Pokémon's current status.
196
+ status: u8 = 0,
197
+ /// The Pokémon's original species.
198
+ species: Species = .None,
199
+ /// The Pokémon's original types.
200
+ types: Types = .{},
201
+ /// The Pokémon's level.
202
+ level: u8 = 100,
203
+
204
+ comptime {
205
+ assert(@sizeOf(Pokemon) == 24);
206
+ }
207
+
208
+ /// Returns the Pokémon's original move slot located at the one-indexed `mslot`.
209
+ pub fn move(self: anytype, mslot: u8) PointerType(@TypeOf(self), MoveSlot) {
210
+ assert(@typeInfo(@TypeOf(self)).Pointer.child == Pokemon);
211
+ assert(mslot > 0 and mslot <= 4);
212
+ assert(self.moves[mslot - 1].id != .None);
213
+ return &self.moves[mslot - 1];
214
+ }
215
+ };
216
+
217
+ /// Representation of a Generation I Pokémon's move slot in a battle.
218
+ pub const MoveSlot = extern struct {
219
+ /// The identifier for the move.
220
+ id: Move = .None,
221
+ /// The remaining PP of the move.
222
+ pp: u8 = 0,
223
+
224
+ comptime {
225
+ assert(@sizeOf(MoveSlot) == 2);
226
+ }
227
+ };
228
+
229
+ /// Details required to detect desyncs based on the move last selected/executed by players.
230
+ pub const MoveDetails = packed struct(if (showdown) u16 else u8) {
231
+ /// The move slot index of the last move selected in the battle menu.
232
+ index: if (showdown) u8 else u4 = 0,
233
+ /// Whether the last move executed was counterable.
234
+ counterable: if (showdown) u8 else u4 = 0,
235
+ };
236
+
237
+ /// Bitfield representation of a Generation I & II Pokémon's major status condition.
238
+ pub const Status = enum(u8) {
239
+ // 0 and 1 bits are also used for SLP
240
+ SLP = 2,
241
+ PSN = 3,
242
+ BRN = 4,
243
+ FRZ = 5,
244
+ PAR = 6,
245
+ // In Generation I & II, the "badly poisoned" status is marked with Volatiles.Toxic instead of
246
+ // being via a TOX status, however we need to use this top bit for Pokémon Showdown quirks:
247
+ //
248
+ // - when combined with a valid sleep duration (or 0), it indicates that the `SLP` status was
249
+ // self-inflicted (required in order to implement Pokémon Showdown's "Sleep Clause Mod")
250
+ // - when combined with `PSN` it indicates that the Pokémon is actually badly poisoned
251
+ // (required in order to send the same incorrect protocol messages as Pokémon Showdown - the
252
+ // Toxic volatile alone is not sufficient in compatibility mode because it gets lost on
253
+ // switch)
254
+ EXT = 7,
255
+
256
+ pub const TOX = 0b10001000;
257
+
258
+ const SLP = 0b111;
259
+
260
+ /// Whether or not the status `num` is the same as `status`.
261
+ pub fn is(num: u8, status: Status) bool {
262
+ if (status == .SLP) return Status.duration(num) > 0;
263
+ return ((num >> @intCast(@intFromEnum(status))) & 1) != 0;
264
+ }
265
+
266
+ /// Initializes a non-sleep `status`. Use `slp` or `slf` to initialize a sleep status.
267
+ pub fn init(status: Status) u8 {
268
+ assert(status != .SLP and status != .EXT);
269
+ return @as(u8, 1) << @intCast(@intFromEnum(status));
270
+ }
271
+
272
+ /// Initializes a non self-inflicted sleep status with duration `dur`.
273
+ pub fn slp(dur: u3) u8 {
274
+ assert(dur > 0);
275
+ return @as(u8, dur);
276
+ }
277
+
278
+ /// Initializes a self-inflicted sleep status with duration `dur`.
279
+ pub fn slf(dur: u3) u8 {
280
+ assert(dur > 0);
281
+ return 0x80 | slp(dur);
282
+ }
283
+
284
+ /// Returns the duration of a sleep status.
285
+ pub fn duration(num: u8) u3 {
286
+ return @intCast(num & SLP);
287
+ }
288
+
289
+ /// Returns whether `num` reflects any status.
290
+ pub fn any(num: u8) bool {
291
+ return num > 0;
292
+ }
293
+
294
+ /// Returns a human-readable representation of the status `num`.
295
+ pub fn name(num: u8) []const u8 {
296
+ if (Status.is(num, .EXT)) return if (num == Status.TOX) "TOX" else "SLF";
297
+ if (Status.is(num, .SLP)) return "SLP";
298
+ if (Status.is(num, .PSN)) return "PSN";
299
+ if (Status.is(num, .BRN)) return "BRN";
300
+ if (Status.is(num, .FRZ)) return "FRZ";
301
+ if (Status.is(num, .PAR)) return "PAR";
302
+ return "OK";
303
+ }
304
+ };
305
+
306
+ test Status {
307
+ try expect(!Status.any(0));
308
+
309
+ try expect(Status.is(Status.init(.PSN), .PSN));
310
+ try expect(!Status.is(Status.init(.PSN), .PAR));
311
+ try expect(Status.is(Status.init(.BRN), .BRN));
312
+ try expect(!Status.is(Status.init(.FRZ), .SLP));
313
+ try expect(Status.is(Status.init(.FRZ), .FRZ));
314
+
315
+ try expect(!Status.is(0, .SLP));
316
+ try expect(Status.is(Status.slp(5), .SLP));
317
+ try expect(!Status.is(Status.slp(5), .EXT));
318
+ try expect(!Status.is(Status.slp(7), .PSN));
319
+ try expectEqual(@as(u3, 5), Status.duration(Status.slp(5)));
320
+ try expect(Status.is(Status.slf(2), .SLP));
321
+ try expect(Status.is(Status.slf(2), .EXT));
322
+ try expect(Status.is(Status.TOX, .PSN));
323
+ try expect(Status.is(Status.TOX, .EXT));
324
+ try expectEqual(@as(u3, 1), Status.duration(Status.slp(1)));
325
+ }
326
+
327
+ /// Bitfield representation of volatile statuses and associated data in Generation I.
328
+ pub const Volatiles = packed struct(u64) {
329
+ /// Whether the "Bide" volatile status is present.
330
+ Bide: bool = false,
331
+ /// Whether the "Thrashing" volatile status is present.
332
+ Thrashing: bool = false,
333
+ /// Whether the "MultiHit" volatile status is present.
334
+ MultiHit: bool = false,
335
+ /// Whether the "Flinch" volatile status is present.
336
+ Flinch: bool = false,
337
+ /// Whether the "Charging" volatile status is present.
338
+ Charging: bool = false,
339
+ /// Whether the "Binding" volatile status is present.
340
+ Binding: bool = false,
341
+ /// Whether the "Invulnerable" volatile status is present.
342
+ Invulnerable: bool = false,
343
+ /// Whether the "Confusion" volatile status is present.
344
+ Confusion: bool = false,
345
+
346
+ /// Whether the "Mist" volatile status is present.
347
+ Mist: bool = false,
348
+ /// Whether the "FocusEnergy" volatile status is present.
349
+ FocusEnergy: bool = false,
350
+ /// Whether the "Substitute" volatile status is present.
351
+ Substitute: bool = false,
352
+ /// Whether the "Recharging" volatile status is present.
353
+ Recharging: bool = false,
354
+ /// Whether the "Rage" volatile status is present.
355
+ Rage: bool = false,
356
+ /// Whether the "LeechSeed" volatile status is present.
357
+ LeechSeed: bool = false,
358
+ /// Whether the "Toxic" volatile status is present.
359
+ Toxic: bool = false,
360
+ /// Whether the "LightScreen" volatile status is present.
361
+ LightScreen: bool = false,
362
+
363
+ /// Whether the "Reflect" volatile status is present.
364
+ Reflect: bool = false,
365
+ /// Whether the "Transform" volatile status is present.
366
+ Transform: bool = false,
367
+ /// The remaining turns of confusion
368
+ confusion: u3 = 0,
369
+ /// The number of attacks remaining
370
+ attacks: u3 = 0,
371
+
372
+ /// A union of either:
373
+ /// - the total accumulated damage from Bide
374
+ /// - the overwritten accuracy of certain moves
375
+ state: u16 = 0,
376
+ /// The remaining HP of the Substitute.
377
+ substitute: u8 = 0,
378
+ /// The identity of whom the active Pokémon is transformed into.
379
+ transform: u4 = 0,
380
+ /// The remaining turns the move is disabled.
381
+ disable_duration: u4 = 0,
382
+ /// The move slot (1-4) that is disabled.
383
+ disable_move: u3 = 0,
384
+ /// The number of turns toxic damage has been accumulating.
385
+ toxic: u5 = 0,
386
+ };
387
+
388
+ test Volatiles {
389
+ var volatiles: Volatiles = .{};
390
+ volatiles.Confusion = true;
391
+ volatiles.confusion = 2;
392
+ volatiles.Thrashing = true;
393
+ volatiles.state = 235;
394
+ volatiles.attacks = 3;
395
+ volatiles.Substitute = true;
396
+ volatiles.substitute = 42;
397
+ volatiles.toxic = 4;
398
+ volatiles.disable_move = 2;
399
+ volatiles.disable_duration = 4;
400
+
401
+ try expect(volatiles.Confusion);
402
+ try expect(volatiles.Thrashing);
403
+ try expect(volatiles.Substitute);
404
+ try expect(!volatiles.Recharging);
405
+ try expect(!volatiles.Transform);
406
+ try expect(!volatiles.MultiHit);
407
+
408
+ try expectEqual(@as(u16, 235), volatiles.state);
409
+ try expectEqual(@as(u8, 42), volatiles.substitute);
410
+ try expectEqual(@as(u4, 2), volatiles.disable_move);
411
+ try expectEqual(@as(u4, 4), volatiles.disable_duration);
412
+ try expectEqual(@as(u4, 2), volatiles.confusion);
413
+ try expectEqual(@as(u5, 4), volatiles.toxic);
414
+ try expectEqual(@as(u4, 3), volatiles.attacks);
415
+ }
416
+
417
+ /// Representaiton of a Pokémon's stats in Generation I.
418
+ pub fn Stats(comptime T: type) type {
419
+ return extern struct {
420
+ hp: T = 0,
421
+ atk: T = 0,
422
+ def: T = 0,
423
+ spe: T = 0,
424
+ spc: T = 0,
425
+
426
+ /// Computes the value of `stat` given a `base`, `dv`, stat `exp` and `level`.
427
+ pub fn calc(comptime stat: []const u8, base: T, dv: u4, exp: u16, level: u8) T {
428
+ assert(level > 0 and level <= 100);
429
+ const float: f32 = @floatFromInt(exp);
430
+ const evs: u16 = @min(255, @as(u16, @intFromFloat(@ceil(@sqrt(float)))));
431
+ const core: u32 = (2 *% (@as(u32, base) +% dv)) +% (evs / 4);
432
+ const factor: u32 = if (std.mem.eql(u8, stat, "hp")) level + 10 else 5;
433
+ return @intCast(core *% @as(u32, level) / 100 +% factor);
434
+ }
435
+ };
436
+ }
437
+
438
+ test Stats {
439
+ try expectEqual(5, @sizeOf(Stats(u8)));
440
+ const stats = Stats(u16){ .atk = 2, .spe = 3 };
441
+ try expectEqual(2, stats.atk);
442
+ try expectEqual(0, stats.def);
443
+
444
+ var base = Species.get(.Gyarados).stats;
445
+ try expectEqual(@as(u16, 127), Stats(u16).calc("hp", base.hp, 0, 5120, 38));
446
+ base = Species.get(.Pidgeot).stats;
447
+ try expectEqual(@as(u16, 279), Stats(u16).calc("spe", base.spe, 15, 63001, 100));
448
+ }
449
+
450
+ /// Representation of a Pokémon's boosts in Generation I.
451
+ pub const Boosts = packed struct(u32) {
452
+ /// A Pokémon's Attack boosts.
453
+ atk: i4 = 0,
454
+ /// A Pokémon's Defense boosts.
455
+ def: i4 = 0,
456
+ /// A Pokémon's Speed boosts.
457
+ spe: i4 = 0,
458
+ /// A Pokémon's Special boosts.
459
+ spc: i4 = 0,
460
+ /// A Pokémon's Accuracy boosts.
461
+ accuracy: i4 = 0,
462
+ /// A Pokémon's Evasion boosts.
463
+ evasion: i4 = 0,
464
+
465
+ _: u8 = 0,
466
+ };
467
+
468
+ test Boosts {
469
+ const boosts: Boosts = .{ .spc = -6 };
470
+ try expectEqual(0, boosts.atk);
471
+ try expectEqual(-6, boosts.spc);
472
+ }
473
+
474
+ /// Representation of a Generation I Pokémon move.
475
+ pub const Move = moves.Move;
476
+
477
+ test Move {
478
+ try expectEqual(2, @intFromEnum(Move.KarateChop));
479
+ const move = Move.get(.Fissure);
480
+ try expectEqual(Move.Effect.OHKO, move.effect);
481
+ try expectEqual(rng.Gen12.percent(30), move.accuracy);
482
+ try expectEqual(Type.Ground, move.type);
483
+
484
+ try expect(!Move.Effect.onBegin(.None));
485
+ try expect(Move.Effect.onBegin(.Confusion));
486
+ try expect(Move.Effect.onBegin(.Transform));
487
+ try expect(!Move.Effect.onBegin(.AccuracyDown1));
488
+
489
+ try expect(!Move.Effect.isStatDown(.Transform));
490
+ try expect(Move.Effect.isStatDown(.AccuracyDown1));
491
+ try expect(Move.Effect.isStatDown(.SpeedDown1));
492
+ try expect(!Move.Effect.isStatDown(.AttackUp1));
493
+
494
+ try expect(!Move.Effect.onEnd(.Transform));
495
+ try expect(Move.Effect.onEnd(.AccuracyDown1));
496
+ try expect(Move.Effect.onEnd(.SpeedUp2));
497
+ try expect(!Move.Effect.onEnd(.Charge));
498
+
499
+ try expect(!Move.Effect.alwaysHappens(.SpeedUp2));
500
+ try expect(Move.Effect.alwaysHappens(.DrainHP));
501
+ try expect(Move.Effect.alwaysHappens(.Recoil));
502
+ try expect(!Move.Effect.alwaysHappens(.Charge));
503
+ try expect(!Move.Effect.alwaysHappens(.SpeedDownChance));
504
+ try expect(Move.Effect.alwaysHappens(.Rage));
505
+
506
+ // all considered "always happens" on cartridge
507
+ try expect(!Move.Effect.alwaysHappens(.DoubleHit));
508
+ try expect(!Move.Effect.alwaysHappens(.MultiHit));
509
+ try expect(!Move.Effect.alwaysHappens(.Twineedle));
510
+
511
+ try expect(!Move.Effect.isSpecial(.SpeedUp2));
512
+ try expect(Move.Effect.isSpecial(.DrainHP));
513
+ try expect(Move.Effect.isSpecial(.Rage)); // other on cartridge
514
+ try expect(Move.Effect.isSpecial(.DoubleHit));
515
+ try expect(Move.Effect.isSpecial(.MultiHit));
516
+ try expect(!Move.Effect.isSpecial(.Twineedle));
517
+
518
+ try expect(!Move.Effect.isMulti(.Thrashing));
519
+ try expect(Move.Effect.isMulti(.DoubleHit));
520
+ try expect(Move.Effect.isMulti(.MultiHit));
521
+ try expect(Move.Effect.isMulti(.Twineedle));
522
+ try expect(!Move.Effect.isMulti(.AttackDownChance));
523
+
524
+ try expect(!Move.Effect.isStatDownChance(.Thrashing));
525
+ try expect(Move.Effect.isStatDownChance(.AttackDownChance));
526
+ try expect(Move.Effect.isStatDownChance(.SpecialDownChance));
527
+ try expect(!Move.Effect.isStatDownChance(.BurnChance1));
528
+
529
+ try expect(!Move.Effect.isSecondaryChance(.MultiHit));
530
+ try expect(Move.Effect.isSecondaryChance(.Twineedle));
531
+ try expect(Move.Effect.isSecondaryChance(.PoisonChance2));
532
+ try expect(!Move.Effect.isSecondaryChance(.Disable));
533
+ }
534
+
535
+ /// Representation of a Generation I Pokémon species.
536
+ pub const Species = species.Species;
537
+
538
+ test Species {
539
+ try expectEqual(2, @intFromEnum(Species.Ivysaur));
540
+ try expectEqual(@as(u8, 100), Species.get(.Mew).stats.def);
541
+ }
542
+
543
+ /// Representation of a Generation I type in Pokémon.
544
+ pub const Type = types.Type;
545
+ /// Representation of a Generation I Pokémon's typing.
546
+ pub const Types = types.Types;
547
+
548
+ /// Modifiers for the effectiveness of a type vs. another type in Pokémon.
549
+ pub const Effectiveness = enum(u8) {
550
+ Immune = 0,
551
+ Resisted = 5,
552
+ Neutral = 10,
553
+ Super = 20,
554
+
555
+ /// Used to determine the effectiness of a neutral damage hit.
556
+ pub const neutral: u16 =
557
+ @intFromEnum(Effectiveness.Neutral) * @intFromEnum(Effectiveness.Neutral);
558
+ /// Used to detect whether there is a mismatch in type effectivness (relevant for precedence).
559
+ pub const mismatch: u16 =
560
+ @intFromEnum(Effectiveness.Resisted) + @intFromEnum(Effectiveness.Super);
561
+
562
+ comptime {
563
+ assert(@sizeOf(Effectiveness) == 1);
564
+ }
565
+ };
566
+
567
+ test Types {
568
+ try expectEqual(14, @intFromEnum(Type.Dragon));
569
+ try expectEqual(20, @intFromEnum(Effectiveness.Super));
570
+
571
+ try expect(!Type.Ghost.special());
572
+ try expect(Type.Dragon.special());
573
+
574
+ try expectEqual(Effectiveness.Immune, Type.effectiveness(.Ghost, .Psychic));
575
+ try expectEqual(Effectiveness.Super, Type.Water.effectiveness(.Fire));
576
+ try expectEqual(Effectiveness.Resisted, Type.effectiveness(.Fire, .Water));
577
+ try expectEqual(Effectiveness.Neutral, Type.effectiveness(.Normal, .Grass));
578
+
579
+ try expectEqual(@as(u8, 3), Type.precedence(.Water, .Water));
580
+ try expectEqual(@as(u8, 26), Type.precedence(.Bug, .Ghost));
581
+ try expect(Type.precedence(.Poison, .Bug) > Type.precedence(.Poison, .Poison));
582
+
583
+ const t: Types = .{ .type1 = .Rock, .type2 = .Ground };
584
+ try expect(!t.immune(.Grass));
585
+ try expect(t.immune(.Electric));
586
+
587
+ try expect(!t.includes(.Fire));
588
+ try expect(t.includes(.Rock));
589
+ }
590
+
591
+ /// Representation of a Generation I Pokémon's determinant values.
592
+ pub const DVs = struct {
593
+ /// The Attack DV.
594
+ atk: u4 = 15,
595
+ /// The Defense DV.
596
+ def: u4 = 15,
597
+ /// The Speed DV.
598
+ spe: u4 = 15,
599
+ /// The Special DV.
600
+ spc: u4 = 15,
601
+
602
+ /// The computed HP DV.
603
+ pub fn hp(self: DVs) u4 {
604
+ return (self.atk & 1) << 3 | (self.def & 1) << 2 | (self.spe & 1) << 1 | (self.spc & 1);
605
+ }
606
+
607
+ /// Produces a random set of DVs given `rand`.
608
+ pub fn random(rand: *rng.PSRNG) DVs {
609
+ return .{
610
+ .atk = if (rand.chance(u8, 1, 5)) rand.range(u4, 1, 15 + 1) else 15,
611
+ .def = if (rand.chance(u8, 1, 5)) rand.range(u4, 1, 15 + 1) else 15,
612
+ .spe = if (rand.chance(u8, 1, 5)) rand.range(u4, 1, 15 + 1) else 15,
613
+ .spc = if (rand.chance(u8, 1, 5)) rand.range(u4, 1, 15 + 1) else 15,
614
+ };
615
+ }
616
+ };
617
+
618
+ test DVs {
619
+ var dvs: DVs = .{ .spc = 15, .spe = 15 };
620
+ try expectEqual(@as(u4, 15), dvs.hp());
621
+ dvs = .{
622
+ .atk = 5,
623
+ .def = 15,
624
+ .spe = 13,
625
+ .spc = 13,
626
+ };
627
+ try expectEqual(@as(u4, 15), dvs.hp());
628
+ dvs = .{
629
+ .def = 3,
630
+ .spe = 10,
631
+ .spc = 11,
632
+ };
633
+ try expectEqual(@as(u4, 13), dvs.hp());
634
+ }