@pkmn/engine 0.1.0-dev.abd68573 → 0.1.0-dev.ac9225d5
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.
- package/README.md +8 -6
- package/build.zig +13 -29
- package/package.json +9 -9
- package/src/lib/common/debug.zig +2 -2
- package/src/lib/common/options.zig +2 -0
- package/src/lib/common/rational.zig +69 -12
- package/src/lib/gen1/calc.zig +210 -40
- package/src/lib/gen1/chance.zig +61 -63
- package/src/lib/gen1/data.zig +3 -3
- package/src/lib/gen1/helpers.zig +90 -29
- package/src/lib/gen1/mechanics.zig +55 -54
- package/src/lib/gen2/helpers.zig +37 -7
- package/src/test/display/pkmn.html.tmpl +5 -5
package/README.md
CHANGED
|
@@ -88,7 +88,9 @@ directly or build the addons manually and place the artifacts in the expected pa
|
|
|
88
88
|
### `pkmn`
|
|
89
89
|
|
|
90
90
|
To include `pkmn`, add it as a dependency in your project's
|
|
91
|
-
[`build.zig.zon`](https://github.com/ziglang/zig/blob/master/doc/build.zig.zon.md)
|
|
91
|
+
[`build.zig.zon`](https://github.com/ziglang/zig/blob/master/doc/build.zig.zon.md) using [Zig's
|
|
92
|
+
Package Management
|
|
93
|
+
system](https://ziglang.org/download/0.11.0/release-notes.html#Package-Management):
|
|
92
94
|
|
|
93
95
|
|
|
94
96
|
```zig
|
|
@@ -117,18 +119,18 @@ your `build.zig.zon` you will be able to import the `pkmn` package's build helpe
|
|
|
117
119
|
|
|
118
120
|
```zig
|
|
119
121
|
const std = @import("std");
|
|
120
|
-
const pkmn = @import("pkmn");
|
|
121
122
|
|
|
122
123
|
pub fn build(b: *std.Build) void {
|
|
123
124
|
...
|
|
124
|
-
|
|
125
|
+
const pkmn = b.dependency("pkmn", .{ .showdown = true, .log = true });
|
|
126
|
+
exe.root_module.addImport("pkmn", pkmn.module("pkmn"));
|
|
125
127
|
...
|
|
126
128
|
}
|
|
127
129
|
```
|
|
128
130
|
|
|
129
|
-
The `pkmn` package
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
The `pkmn` package can be configured with various options such as whether or not Pokémon Showdown
|
|
132
|
+
compatibility mode or protocol message logging should be enabled. Alternatively, you may set options
|
|
133
|
+
via a `pkmn_options` [root source file
|
|
132
134
|
declaration](https://ziglang.org/documentation/master/#Root-Source-File). There are several
|
|
133
135
|
undocumented internal options that can be tweaked as well via build or root options, though these
|
|
134
136
|
options aren't officially supported, affect correctness, and may change meaning or behavior without
|
package/build.zig
CHANGED
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
const builtin = @import("builtin");
|
|
3
3
|
|
|
4
|
-
pub const Options = struct {
|
|
5
|
-
showdown: ?bool = null,
|
|
6
|
-
log: ?bool = null,
|
|
7
|
-
chance: ?bool = null,
|
|
8
|
-
calc: ?bool = null,
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
pub fn module(b: *std.Build, options: Options) *std.Build.Module {
|
|
12
|
-
const dirname = comptime std.fs.path.dirname(@src().file) orelse ".";
|
|
13
|
-
const build_options = b.addOptions();
|
|
14
|
-
build_options.addOption(?bool, "showdown", options.showdown);
|
|
15
|
-
build_options.addOption(?bool, "log", options.log);
|
|
16
|
-
build_options.addOption(?bool, "chance", options.chance);
|
|
17
|
-
build_options.addOption(?bool, "calc", options.calc);
|
|
18
|
-
return b.createModule(.{
|
|
19
|
-
.root_source_file = .{ .cwd_relative = dirname ++ "/src/lib/pkmn.zig" },
|
|
20
|
-
.imports = &.{.{ .name = "build_options", .module = build_options.createModule() }},
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
4
|
pub fn build(b: *std.Build) !void {
|
|
25
5
|
const target = b.standardTargetOptions(.{});
|
|
26
6
|
const optimize = b.standardOptimizeOption(.{});
|
|
@@ -61,6 +41,13 @@ pub fn build(b: *std.Build) !void {
|
|
|
61
41
|
|
|
62
42
|
const name = if (showdown) "pkmn-showdown" else "pkmn";
|
|
63
43
|
|
|
44
|
+
const module = b.addModule("pkmn", .{
|
|
45
|
+
.root_source_file = b.path("src/lib/pkmn.zig"),
|
|
46
|
+
.optimize = optimize,
|
|
47
|
+
.target = target,
|
|
48
|
+
.imports = &.{.{ .name = "build_options", .module = options.createModule() }},
|
|
49
|
+
});
|
|
50
|
+
|
|
64
51
|
var c = false;
|
|
65
52
|
if (node_headers) |headers| {
|
|
66
53
|
const addon = b.fmt("{s}.node", .{name});
|
|
@@ -222,12 +209,8 @@ pub fn build(b: *std.Build) !void {
|
|
|
222
209
|
|
|
223
210
|
var exes = std.ArrayList(*std.Build.Step.Compile).init(b.allocator);
|
|
224
211
|
const tools: ToolConfig = .{
|
|
225
|
-
.
|
|
226
|
-
|
|
227
|
-
.log = log,
|
|
228
|
-
.chance = chance,
|
|
229
|
-
.calc = calc,
|
|
230
|
-
},
|
|
212
|
+
.showdown = showdown,
|
|
213
|
+
.module = module,
|
|
231
214
|
.general = config,
|
|
232
215
|
.tool = .{
|
|
233
216
|
.tests = if (tests.build) tests else null,
|
|
@@ -326,7 +309,8 @@ const TestStep = struct {
|
|
|
326
309
|
};
|
|
327
310
|
|
|
328
311
|
const ToolConfig = struct {
|
|
329
|
-
|
|
312
|
+
showdown: ?bool,
|
|
313
|
+
module: *std.Build.Module,
|
|
330
314
|
general: Config,
|
|
331
315
|
tool: struct {
|
|
332
316
|
tests: ?*TestStep,
|
|
@@ -343,7 +327,7 @@ fn tool(b: *std.Build, path: []const u8, config: ToolConfig) !?*std.Build.Step.R
|
|
|
343
327
|
var name = config.tool.name orelse std.fs.path.basename(path);
|
|
344
328
|
const index = std.mem.lastIndexOfScalar(u8, name, '.');
|
|
345
329
|
if (index) |i| name = name[0..i];
|
|
346
|
-
if (config.
|
|
330
|
+
if (config.showdown orelse false) name = b.fmt("{s}-showdown", .{name});
|
|
347
331
|
|
|
348
332
|
const exe = b.addExecutable(.{
|
|
349
333
|
.name = name,
|
|
@@ -354,7 +338,7 @@ fn tool(b: *std.Build, path: []const u8, config: ToolConfig) !?*std.Build.Step.R
|
|
|
354
338
|
.strip = config.general.strip,
|
|
355
339
|
.pic = config.general.pic,
|
|
356
340
|
});
|
|
357
|
-
exe.root_module.addImport("pkmn",
|
|
341
|
+
exe.root_module.addImport("pkmn", config.module);
|
|
358
342
|
|
|
359
343
|
if (config.tool.tests) |ts| ts.step.dependOn(&exe.step);
|
|
360
344
|
config.tool.exes.append(exe) catch @panic("OOM");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pkmn/engine",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.ac9225d5",
|
|
4
4
|
"description": "A minimal, complete, Pokémon battle simulation engine optimized for performance",
|
|
5
5
|
"repository": "github:pkmn/engine",
|
|
6
6
|
"license": "MIT",
|
|
@@ -75,20 +75,20 @@
|
|
|
75
75
|
"access": "public"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@pkmn/data": "^0.9.
|
|
78
|
+
"@pkmn/data": "^0.9.11",
|
|
79
79
|
"@pkmn/protocol": "^0.6.25"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@pkmn/eslint-config": "^
|
|
83
|
-
"@pkmn/img": "^0.2.
|
|
82
|
+
"@pkmn/eslint-config": "^8.1.0",
|
|
83
|
+
"@pkmn/img": "^0.2.31",
|
|
84
84
|
"@pkmn/sets": "^5.1.2",
|
|
85
|
-
"@pkmn/sim": "0.9.
|
|
85
|
+
"@pkmn/sim": "0.9.11",
|
|
86
86
|
"@types/html-minifier": "^4.0.5",
|
|
87
87
|
"@types/jest": "^29.5.12",
|
|
88
88
|
"@types/minimist": "^1.2.5",
|
|
89
89
|
"@types/mustache": "^4.2.5",
|
|
90
|
-
"@types/node": "^
|
|
91
|
-
"@vitest/coverage-v8": "^2.0.
|
|
90
|
+
"@types/node": "^22.0.2",
|
|
91
|
+
"@vitest/coverage-v8": "^2.0.5",
|
|
92
92
|
"binaryen": "^118.0.0",
|
|
93
93
|
"eslint": "<=8",
|
|
94
94
|
"html-minifier": "^4.0.0",
|
|
@@ -98,8 +98,8 @@
|
|
|
98
98
|
"source-map-support": "^0.5.21",
|
|
99
99
|
"table": "^6.8.2",
|
|
100
100
|
"trakr": "^0.2.0",
|
|
101
|
-
"typescript": "^5.5.
|
|
102
|
-
"vitest": "^2.0.
|
|
101
|
+
"typescript": "^5.5.4",
|
|
102
|
+
"vitest": "^2.0.5"
|
|
103
103
|
},
|
|
104
104
|
"scripts": {
|
|
105
105
|
"lint": "eslint --cache src --ext ts,js && eslint src/bin/*",
|
package/src/lib/common/debug.zig
CHANGED
|
@@ -4,8 +4,8 @@ const debug = std.debug;
|
|
|
4
4
|
const io = std.io;
|
|
5
5
|
|
|
6
6
|
pub fn print(value: anytype) void {
|
|
7
|
-
debug.
|
|
8
|
-
defer debug.
|
|
7
|
+
debug.lockStdErr();
|
|
8
|
+
defer debug.unlockStdErr();
|
|
9
9
|
const stderr = io.getStdErr().writer();
|
|
10
10
|
|
|
11
11
|
nosuspend {
|
|
@@ -10,6 +10,7 @@ pub const Options = struct {
|
|
|
10
10
|
advance: ?bool = null,
|
|
11
11
|
ebc: ?bool = null,
|
|
12
12
|
key: ?bool = null,
|
|
13
|
+
miss: ?bool = null,
|
|
13
14
|
internal: ?bool = null,
|
|
14
15
|
};
|
|
15
16
|
|
|
@@ -25,6 +26,7 @@ pub const calc = get("calc", false);
|
|
|
25
26
|
pub const advance = get("advance", true);
|
|
26
27
|
pub const ebc = get("ebc", true);
|
|
27
28
|
pub const key = get("key", false);
|
|
29
|
+
pub const miss = get("miss", true);
|
|
28
30
|
pub const internal = get("internal", false);
|
|
29
31
|
|
|
30
32
|
fn get(comptime name: []const u8, default: bool) bool {
|
|
@@ -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
|
-
//
|
|
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
|
|
|
@@ -202,23 +201,81 @@ pub fn Rational(comptime T: type) type {
|
|
|
202
201
|
};
|
|
203
202
|
}
|
|
204
203
|
|
|
205
|
-
// https://
|
|
204
|
+
// https://lemire.me/blog/greatest-common-divisor-the-extended-euclidean-algorithm-and-speed/
|
|
206
205
|
fn gcd(p: anytype, q: anytype) @TypeOf(p, q) {
|
|
207
206
|
assert(p >= 1);
|
|
208
207
|
assert(q >= 1);
|
|
209
208
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
const T = @TypeOf(p, q);
|
|
210
|
+
switch (@typeInfo(T)) {
|
|
211
|
+
.Int => {
|
|
212
|
+
var u: T = undefined;
|
|
213
|
+
var v: T = undefined;
|
|
214
|
+
if (p < q) {
|
|
215
|
+
u = q;
|
|
216
|
+
v = p;
|
|
217
|
+
} else {
|
|
218
|
+
u = p;
|
|
219
|
+
v = q;
|
|
220
|
+
}
|
|
221
|
+
assert(v <= u);
|
|
222
|
+
u %= v;
|
|
223
|
+
if (u == 0) return v;
|
|
224
|
+
|
|
225
|
+
const zu = @ctz(u);
|
|
226
|
+
const zv = @ctz(v);
|
|
227
|
+
const shift = @min(zu, zv);
|
|
228
|
+
u >>= @intCast(zu);
|
|
229
|
+
v >>= @intCast(zv);
|
|
230
|
+
|
|
231
|
+
while (true) {
|
|
232
|
+
const diff = u -% v;
|
|
233
|
+
if (u > v) {
|
|
234
|
+
u = v;
|
|
235
|
+
v = diff;
|
|
236
|
+
} else {
|
|
237
|
+
v -= u;
|
|
238
|
+
}
|
|
239
|
+
if (diff != 0) v >>= @intCast(@ctz(diff));
|
|
240
|
+
if (v == 0) break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const result = u << @intCast(shift);
|
|
244
|
+
assert(result > 0);
|
|
245
|
+
return result;
|
|
246
|
+
},
|
|
247
|
+
else => {
|
|
248
|
+
var a = p;
|
|
249
|
+
var b = q;
|
|
250
|
+
var c: T = undefined;
|
|
251
|
+
|
|
252
|
+
while (b != 0) {
|
|
253
|
+
c = b;
|
|
254
|
+
b = @mod(a, b);
|
|
255
|
+
a = c;
|
|
256
|
+
}
|
|
213
257
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
a = c;
|
|
258
|
+
assert(a > 0);
|
|
259
|
+
return a;
|
|
260
|
+
},
|
|
218
261
|
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
test gcd {
|
|
265
|
+
const seed = if (@hasDecl(std.testing, "random_seed")) std.testing.random_seed else 0x12345678;
|
|
266
|
+
var prng = std.Random.DefaultPrng.init(seed);
|
|
267
|
+
var random = prng.random();
|
|
219
268
|
|
|
220
|
-
|
|
221
|
-
|
|
269
|
+
for (0..1000) |_| {
|
|
270
|
+
const a = random.int(u32);
|
|
271
|
+
const b = random.int(u32);
|
|
272
|
+
if (a == 0 or b == 0) continue;
|
|
273
|
+
|
|
274
|
+
try expectEqual(
|
|
275
|
+
gcd(a, b),
|
|
276
|
+
@as(u32, @intFromFloat(gcd(@as(f64, @floatFromInt(a)), @as(f64, @floatFromInt(b))))),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
222
279
|
}
|
|
223
280
|
|
|
224
281
|
fn doTurn(r: anytype) !void {
|
package/src/lib/gen1/calc.zig
CHANGED
|
@@ -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.
|
|
@@ -69,8 +72,10 @@ pub const Summary = extern struct {
|
|
|
69
72
|
/// The final computed damage that gets applied to the Pokémon. May exceed the target's HP
|
|
70
73
|
// (to determine the *actual* damage done compare the target's stored HP before and after).
|
|
71
74
|
final: u16 = 0,
|
|
72
|
-
/// Whether higher damage will saturate / result in the same outcome (e.g. additional
|
|
73
|
-
/// gets ignored due to it already breaking a Substitute or causing the target
|
|
75
|
+
/// Whether higher direct damage will saturate / result in the same outcome (e.g. additional
|
|
76
|
+
/// direct damage gets ignored due to it already breaking a Substitute or causing the target
|
|
77
|
+
/// to faint). Note that this field does not get set in scenarios where the target would
|
|
78
|
+
/// only be guaranteed to faint due to some sort of subsequent recoil or residual damage.
|
|
74
79
|
capped: bool = false,
|
|
75
80
|
|
|
76
81
|
// NOTE: 15 bits padding
|
|
@@ -201,6 +206,8 @@ pub const Options = struct {
|
|
|
201
206
|
metronome: bool = false,
|
|
202
207
|
};
|
|
203
208
|
|
|
209
|
+
pub const MAX_FRONTIER = 19;
|
|
210
|
+
|
|
204
211
|
pub fn transitions(
|
|
205
212
|
battle: anytype,
|
|
206
213
|
c1: Choice,
|
|
@@ -253,7 +260,13 @@ pub fn transitions(
|
|
|
253
260
|
.p2 = template.p2.durations,
|
|
254
261
|
};
|
|
255
262
|
|
|
256
|
-
try debug(writer, template,
|
|
263
|
+
try debug(writer, template, .{
|
|
264
|
+
.shape = true,
|
|
265
|
+
.color = i,
|
|
266
|
+
.bold = true,
|
|
267
|
+
.background = true,
|
|
268
|
+
.indent = false,
|
|
269
|
+
});
|
|
257
270
|
|
|
258
271
|
var a: Actions = .{ .p1 = .{ .metronome = p1_move }, .p2 = .{ .metronome = p2_move } };
|
|
259
272
|
|
|
@@ -281,7 +294,7 @@ pub fn transitions(
|
|
|
281
294
|
for (Rolls.moveSlot(template.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
|
|
282
295
|
for (Rolls.moveSlot(template.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
|
|
283
296
|
for (Rolls.multiHit(template.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
|
|
284
|
-
for (Rolls.multiHit(template.p2,
|
|
297
|
+
for (Rolls.multiHit(template.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
|
|
285
298
|
for (Rolls.secondaryChance(template.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
|
|
286
299
|
for (Rolls.secondaryChance(template.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
|
|
287
300
|
for (Rolls.criticalHit(template.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
|
|
@@ -290,13 +303,17 @@ pub fn transitions(
|
|
|
290
303
|
var p1_dmg = Rolls.damage(template.p1, p1_hit);
|
|
291
304
|
while (p1_dmg.min < p1_dmg.max) : (p1_dmg.min += 1) {
|
|
292
305
|
a.p1.damage = @intCast(p1_dmg.min);
|
|
293
|
-
var p1_min: u8 = 0;
|
|
294
306
|
|
|
295
307
|
var p2_dmg = Rolls.damage(template.p2, p2_hit);
|
|
308
|
+
|
|
309
|
+
const p1_min: u9 = p1_dmg.min;
|
|
310
|
+
const p2_min: u9 = p2_dmg.min;
|
|
311
|
+
|
|
296
312
|
while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
|
|
297
313
|
a.p2.damage = @intCast(p2_dmg.min);
|
|
298
314
|
|
|
299
315
|
opts.calc.overrides.actions = a;
|
|
316
|
+
opts.calc.summaries = .{};
|
|
300
317
|
opts.chance = .{ .probability = .{}, .actions = actions };
|
|
301
318
|
const q = &opts.chance.probability;
|
|
302
319
|
|
|
@@ -304,28 +321,40 @@ pub fn transitions(
|
|
|
304
321
|
_ = try b.update(c1, c2, &opts);
|
|
305
322
|
stats.updates += 1;
|
|
306
323
|
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// try Rolls.coalesce(.P2, @intCast(u8, p2_dmg.min), &opts.calc.summaries, cap);
|
|
324
|
+
const summaries = &opts.calc.summaries;
|
|
325
|
+
const p1_max: u9 = if (p2_dmg.min != p2_min)
|
|
326
|
+
p1_dmg.min
|
|
327
|
+
else
|
|
328
|
+
try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
|
|
329
|
+
const p2_max: u9 =
|
|
330
|
+
try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
|
|
315
331
|
|
|
316
332
|
if (opts.chance.actions.matches(template)) {
|
|
317
333
|
if (!opts.chance.actions.eql(a)) {
|
|
318
|
-
|
|
334
|
+
if (!summary) {
|
|
335
|
+
try debug(writer, opts.chance.actions, .{
|
|
336
|
+
.p1_max = p1_max,
|
|
337
|
+
.p2_max = p2_max,
|
|
338
|
+
.color = i,
|
|
339
|
+
.dim = true,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
319
342
|
|
|
320
|
-
|
|
343
|
+
p1_dmg.min = p1_max;
|
|
321
344
|
p2_dmg.min = p2_max;
|
|
322
345
|
continue;
|
|
323
346
|
}
|
|
324
347
|
|
|
325
|
-
|
|
348
|
+
if (!summary) {
|
|
349
|
+
try debug(writer, opts.chance.actions, .{
|
|
350
|
+
.p1_max = p1_max,
|
|
351
|
+
.p2_max = p2_max,
|
|
352
|
+
.color = i,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
326
355
|
|
|
327
|
-
for (
|
|
328
|
-
for (p2_dmg.min
|
|
356
|
+
for (p1_min..p1_max + 1) |p1d| {
|
|
357
|
+
for (p2_dmg.min..p2_max + 1) |p2d| {
|
|
329
358
|
var acts = opts.chance.actions;
|
|
330
359
|
acts.p1.damage = @intCast(p1d);
|
|
331
360
|
acts.p2.damage = @intCast(p2d);
|
|
@@ -336,7 +365,7 @@ pub fn transitions(
|
|
|
336
365
|
}
|
|
337
366
|
}
|
|
338
367
|
|
|
339
|
-
if (p1_max !=
|
|
368
|
+
if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
|
|
340
369
|
if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
|
|
341
370
|
try p.add(q);
|
|
342
371
|
stats.saved += 1;
|
|
@@ -346,27 +375,46 @@ pub fn transitions(
|
|
|
346
375
|
return error.TestUnexpectedResult;
|
|
347
376
|
}
|
|
348
377
|
} else {
|
|
349
|
-
try debug(writer, opts.chance.actions, false, .{ .dim = true });
|
|
350
|
-
|
|
351
378
|
if (!matches(opts.chance.actions, i, frontier.items)) {
|
|
352
379
|
try frontier.append(opts.chance.actions);
|
|
353
380
|
|
|
354
|
-
try debug(writer, opts.chance.actions,
|
|
381
|
+
try debug(writer, opts.chance.actions, .{
|
|
382
|
+
.p1_max = p1_max,
|
|
383
|
+
.p2_max = p2_max,
|
|
384
|
+
.dim = true,
|
|
385
|
+
.newline = false,
|
|
386
|
+
});
|
|
387
|
+
try writer.writeAll(" → ");
|
|
388
|
+
try debug(writer, opts.chance.actions, .{
|
|
389
|
+
.shape = true,
|
|
355
390
|
.color = frontier.items.len - 1,
|
|
356
391
|
.dim = true,
|
|
357
|
-
.background = true
|
|
392
|
+
.background = true,
|
|
393
|
+
.indent = false,
|
|
394
|
+
});
|
|
395
|
+
} else if (!summary) {
|
|
396
|
+
try debug(writer, opts.chance.actions, .{
|
|
397
|
+
.p1_max = p1_max,
|
|
398
|
+
.p2_max = p2_max,
|
|
399
|
+
.dim = true,
|
|
358
400
|
});
|
|
359
401
|
}
|
|
360
402
|
}
|
|
361
403
|
|
|
362
|
-
|
|
404
|
+
p1_dmg.min = p1_max;
|
|
363
405
|
p2_dmg.min = p2_max;
|
|
364
406
|
}
|
|
365
407
|
|
|
366
|
-
assert(p1_min > 0 or p1_dmg.min == 0);
|
|
367
|
-
p1_dmg.min = p1_min;
|
|
368
|
-
|
|
369
408
|
}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
|
|
409
|
+
|
|
410
|
+
if (@TypeOf(writer) != @TypeOf(std.io.null_writer)) {
|
|
411
|
+
p.reduce();
|
|
412
|
+
try writer.print(
|
|
413
|
+
" = {s} ({d:.2}%)\n\n",
|
|
414
|
+
.{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
370
418
|
}
|
|
371
419
|
|
|
372
420
|
assert(frontier.items.len == i);
|
|
@@ -387,6 +435,85 @@ pub fn transitions(
|
|
|
387
435
|
return stats;
|
|
388
436
|
}
|
|
389
437
|
|
|
438
|
+
pub fn update(
|
|
439
|
+
battle: anytype,
|
|
440
|
+
c1: Choice,
|
|
441
|
+
c2: Choice,
|
|
442
|
+
options: anytype,
|
|
443
|
+
allocator: std.mem.Allocator,
|
|
444
|
+
writer: anytype,
|
|
445
|
+
transition: bool,
|
|
446
|
+
) !Result {
|
|
447
|
+
options.chance.reset();
|
|
448
|
+
|
|
449
|
+
if (!pkmn.options.chance or !pkmn.options.calc or @TypeOf(battle.rng) != data.PRNG) {
|
|
450
|
+
return battle.update(c1, c2, options);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
var copy = battle.*;
|
|
454
|
+
const actions = options.chance.actions;
|
|
455
|
+
|
|
456
|
+
// Perfom the actual update
|
|
457
|
+
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(©, 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;
|
|
472
|
+
|
|
473
|
+
if (transition) {
|
|
474
|
+
// Ensure we can generate all transitions from the same original state
|
|
475
|
+
// (we must change the battle's RNG from a FixedRNG to a PRNG because
|
|
476
|
+
// the transitions function relies on RNG for discovery of states)
|
|
477
|
+
if (try transitions(unfix(copy), c1, c2, allocator, writer, .{
|
|
478
|
+
.actions = actions,
|
|
479
|
+
.cap = true,
|
|
480
|
+
})) |stats| try expect(stats.frontier <= MAX_FRONTIER);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 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.
|
|
486
|
+
var override = pkmn.battle.options(
|
|
487
|
+
protocol.NULL,
|
|
488
|
+
chance.NULL,
|
|
489
|
+
Calc{ .overrides = .{ .actions = actions } },
|
|
490
|
+
);
|
|
491
|
+
const overridden = copy.update(c1, c2, &override);
|
|
492
|
+
try expectEqual(result, overridden);
|
|
493
|
+
|
|
494
|
+
// The actual battle excluding its RNG field should match a copy updated with
|
|
495
|
+
// overridden RNG (the copy RNG may have advanced because of no-ops)
|
|
496
|
+
copy.rng = battle.rng;
|
|
497
|
+
try expectEqual(copy, battle.*);
|
|
498
|
+
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fn unfix(actual: anytype) data.Battle(data.PRNG) {
|
|
503
|
+
return .{
|
|
504
|
+
.sides = actual.sides,
|
|
505
|
+
.turn = actual.turn,
|
|
506
|
+
.last_damage = actual.last_damage,
|
|
507
|
+
.last_moves = actual.last_moves,
|
|
508
|
+
.rng = .{ .src = .{
|
|
509
|
+
.seed = if (pkmn.options.showdown)
|
|
510
|
+
0x12345678
|
|
511
|
+
else
|
|
512
|
+
.{ 123, 234, 56, 78, 9, 101, 112, 131, 4 },
|
|
513
|
+
} },
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
390
517
|
fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
|
|
391
518
|
for (frontier, 0..) |f, j| {
|
|
392
519
|
// TODO: is skipping this redundant check worth it?
|
|
@@ -404,23 +531,66 @@ fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
|
|
|
404
531
|
}
|
|
405
532
|
|
|
406
533
|
const Style = struct {
|
|
534
|
+
shape: bool = false,
|
|
535
|
+
p1_max: u9 = 0,
|
|
536
|
+
p2_max: u9 = 0,
|
|
407
537
|
color: ?usize = null,
|
|
408
538
|
bold: bool = false,
|
|
409
539
|
background: bool = false,
|
|
410
540
|
dim: bool = false,
|
|
541
|
+
newline: bool = true,
|
|
542
|
+
indent: bool = true,
|
|
411
543
|
};
|
|
412
544
|
|
|
413
|
-
fn debug(writer: anytype, actions: Actions,
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
545
|
+
fn debug(writer: anytype, actions: Actions, style: Style) !void {
|
|
546
|
+
if (style.indent) try writer.writeAll(" ");
|
|
547
|
+
if (tty) {
|
|
548
|
+
const mod: usize = if (style.dim) 2 else 1;
|
|
549
|
+
const background: usize = if (style.background) 4 else 3;
|
|
550
|
+
const color: usize = if (style.color) |c| (c % 6) + 1 else 7;
|
|
551
|
+
|
|
552
|
+
if (style.dim or style.bold) try writer.print("\x1b[{d}m", .{mod});
|
|
553
|
+
try writer.print("\x1b[{d}{d}m", .{ background, color });
|
|
554
|
+
const p1 = if (style.p1_max != 0 and style.p1_max != actions.p1.damage) style.p1_max else 0;
|
|
555
|
+
const p2 = if (style.p2_max != 0 and style.p2_max != actions.p2.damage) style.p2_max else 0;
|
|
556
|
+
if (p1 != 0 or p2 != 0) {
|
|
557
|
+
assert(!style.shape);
|
|
558
|
+
try format(writer, actions, p1, p2);
|
|
559
|
+
} else {
|
|
560
|
+
try actions.fmt(writer, style.shape);
|
|
561
|
+
}
|
|
562
|
+
try writer.writeAll("\x1b[0m");
|
|
563
|
+
} else {
|
|
564
|
+
if (style.dim) try writer.writeAll(" ");
|
|
565
|
+
try actions.fmt(writer, style.shape);
|
|
566
|
+
}
|
|
567
|
+
if (style.newline) try writer.writeByte('\n');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
|
|
571
|
+
var input: [1024]u8 = undefined;
|
|
572
|
+
var output: [1024]u8 = undefined;
|
|
573
|
+
|
|
574
|
+
var stream = std.io.fixedBufferStream(&input);
|
|
575
|
+
try actions.fmt(&stream.writer(), false);
|
|
576
|
+
var len = stream.getWritten().len;
|
|
577
|
+
@memcpy(output[0..len], input[0..len]);
|
|
578
|
+
|
|
579
|
+
var find: [16]u8 = undefined;
|
|
580
|
+
var repl: [22]u8 = undefined;
|
|
581
|
+
if (p1 != 0 and actions.p1.damage != 0) {
|
|
582
|
+
const n = try std.fmt.bufPrint(&find, "P1 = (damage:{d}", .{actions.p1.damage});
|
|
583
|
+
const r = try std.fmt.bufPrint(&repl, "P1 = (damage:{d}…{d}", .{ actions.p1.damage, p1 });
|
|
584
|
+
assert(std.mem.replace(u8, input[0..len], n, r, output[0 .. len + 6]) == 1);
|
|
585
|
+
@memcpy(input[0 .. len + 6], output[0 .. len + 6]);
|
|
586
|
+
len += 6;
|
|
587
|
+
}
|
|
588
|
+
if (p2 != 0 and actions.p2.damage != 0) {
|
|
589
|
+
const n = try std.fmt.bufPrint(&find, "P2 = (damage:{d}", .{actions.p2.damage});
|
|
590
|
+
const r = try std.fmt.bufPrint(&repl, "P2 = (damage:{d}…{d}", .{ actions.p2.damage, p2 });
|
|
591
|
+
assert(std.mem.replace(u8, input[0..len], n, r, output[0 .. len + 6]) == 1);
|
|
592
|
+
len += 6;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
try writer.writeAll(output[0..len]);
|
|
426
596
|
}
|