@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.
@@ -166,7 +166,7 @@ fn selectMove(
166
166
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
167
167
  // set the selected move will not actually be used, it will just be reported as having
168
168
  // been used (this differs from how Pokémon Showdown works, but its impossible to
169
- // replicate the incorrect behavior with the correct mechanisms).
169
+ // replicate the incorrect behavior with the correct mechanisms)
170
170
  saveMove(battle, player, choice);
171
171
  } else {
172
172
  assert(choice.data == 0);
@@ -347,10 +347,10 @@ fn doTurn(
347
347
  )) |r| return r;
348
348
  if (!replace) {
349
349
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
350
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
351
  }
352
352
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
353
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
354
  } else if (foe_choice.type == .Pass) return null;
355
355
 
356
356
  options.chance.clearPending();
@@ -369,10 +369,10 @@ fn doTurn(
369
369
  )) |r| return r;
370
370
  if (!replace) {
371
371
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
372
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
373
  }
374
374
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
375
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
376
  }
377
377
 
378
378
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -431,7 +431,7 @@ fn executeMove(
431
431
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
432
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
433
  // thawed, in which case we will desync unless the last_selected_move happened to be at
434
- // index 1 and the current Pokémon has the same move in its first slot.
434
+ // index 1 and the current Pokémon has the same move in its first slot
435
435
  if (!auto) {
436
436
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
437
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -852,7 +852,7 @@ fn doMove(
852
852
  break :miss true;
853
853
  }
854
854
  break :miss (move.target != .Self and
855
- !moveHit(battle, player, move, &immune, &mist, options));
855
+ !try moveHit(battle, player, move, &immune, &mist, options));
856
856
  };
857
857
  assert(!immune or miss or (showdown and move.effect == .Binding));
858
858
 
@@ -899,7 +899,7 @@ fn doMove(
899
899
  miss = if (showdown or skip)
900
900
  miss
901
901
  else
902
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
902
+ (!try moveHit(battle, player, move, &immune, &mist, options) or zero);
903
903
 
904
904
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
905
  assert((!showdown and miss) or !(ohko and immune));
@@ -930,7 +930,7 @@ fn doMove(
930
930
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
931
  try log.fail(.{ foe_ident, .None });
932
932
  } else {
933
- if (showdown or !zero) try options.chance.commit(player, .miss);
933
+ if (!showdown and !zero) try options.chance.commit(player, .miss);
934
934
  try log.lastmiss(.{});
935
935
  try log.miss(.{battle.active(player)});
936
936
  }
@@ -949,13 +949,13 @@ fn doMove(
949
949
  try options.chance.commit(player, .binding);
950
950
  }
951
951
  return null;
952
- } else {
952
+ } else if (!showdown) {
953
953
  try options.chance.commit(player, .hit);
954
954
  }
955
955
 
956
956
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
957
957
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
958
- // it up above before damage calculation).
958
+ // it up above before damage calculation)
959
959
  if (!showdown and move.effect.isMulti()) {
960
960
  try Effects.multiHit(battle, player, move, options);
961
961
  hits = side.active.volatiles.attacks;
@@ -1014,12 +1014,12 @@ fn doMove(
1014
1014
  }
1015
1015
 
1016
1016
  // Substitute being broken nullifies the move's effect completely so even
1017
- // if an effect was intended to "always happen" it will still get skipped.
1017
+ // if an effect was intended to "always happen" it will still get skipped
1018
1018
  if (nullified) return null;
1019
1019
 
1020
1020
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1021
1021
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1022
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1022
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1023
1023
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1024
1024
 
1025
1025
  if (foe.stored().hp == 0) return null;
@@ -1034,7 +1034,7 @@ fn doMove(
1034
1034
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1035
1035
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1036
1036
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1037
- // puts the |-status| message before |-hitcount| instead of after.
1037
+ // puts the |-status| message before |-hitcount| instead of after
1038
1038
  if (move.effect == .Twineedle) {
1039
1039
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1040
1040
  } else if (move.effect == .Disable) {
@@ -1058,7 +1058,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1058
1058
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1059
1059
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1060
1060
 
1061
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1061
+ const spurious =
1062
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1063
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1062
1064
  }
1063
1065
 
1064
1066
  fn calcDamage(
@@ -1173,7 +1175,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1173
1175
  const foe = battle.foe(player);
1174
1176
 
1175
1177
  if (!try checkHit(battle, player, move, options)) return null;
1176
- try options.chance.commit(player, .hit);
1177
1178
 
1178
1179
  battle.last_damage = switch (side.last_selected_move) {
1179
1180
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1234,9 +1235,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1235
 
1235
1236
  battle.last_damage *|= 2;
1236
1237
 
1237
- // Pokémon Showdown calls checkHit before Counter
1238
+ // Pokémon Showdown calls moveHit before Counter
1238
1239
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1239
- try options.chance.commit(player, .hit);
1240
1240
 
1241
1241
  const sub = showdown and foe.active.volatiles.Substitute;
1242
1242
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1265,7 +1265,7 @@ fn applyDamage(
1265
1265
  subbed.active.volatiles.Substitute = false;
1266
1266
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1267
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(target_player);
1268
+ options.calc.capped(sub_player);
1269
1269
  return true;
1270
1270
  } else {
1271
1271
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1332,7 +1332,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1332
1332
  var immune = false;
1333
1333
  var mist = false;
1334
1334
 
1335
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1335
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1336
+ if (!showdown) try options.chance.commit(player, .hit);
1337
+ return true;
1338
+ }
1336
1339
 
1337
1340
  assert(!immune);
1338
1341
  if (mist) {
@@ -1341,11 +1344,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1341
1344
  try options.log.activate(.{ foe_ident, .Mist });
1342
1345
  try options.log.fail(.{ foe_ident, .None });
1343
1346
  } else {
1344
- try options.chance.commit(player, .miss);
1345
1347
  try options.log.lastmiss(.{});
1346
1348
  try options.log.miss(.{battle.active(player)});
1347
1349
  }
1348
1350
 
1351
+ if (!showdown) try options.chance.commit(player, .miss);
1349
1352
  return false;
1350
1353
  }
1351
1354
 
@@ -1356,7 +1359,7 @@ fn moveHit(
1356
1359
  immune: *bool,
1357
1360
  mist: *bool,
1358
1361
  options: anytype,
1359
- ) bool {
1362
+ ) !bool {
1360
1363
  var side = battle.side(player);
1361
1364
  const foe = battle.foe(player);
1362
1365
 
@@ -1407,7 +1410,7 @@ fn moveHit(
1407
1410
  if (overwrite) side.active.volatiles.state = accuracy;
1408
1411
 
1409
1412
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1410
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1413
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1411
1414
  };
1412
1415
 
1413
1416
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1425,6 +1428,7 @@ fn moveHit(
1425
1428
  fn checkFaint(
1426
1429
  battle: anytype,
1427
1430
  player: Player,
1431
+ cap: bool,
1428
1432
  options: anytype,
1429
1433
  ) @TypeOf(options.log).Error!?Result {
1430
1434
  const side = battle.side(player);
@@ -1437,8 +1441,8 @@ fn checkFaint(
1437
1441
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
1442
  const more = player_out or foe_out;
1439
1443
 
1440
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1441
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1444
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1445
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1442
1446
 
1443
1447
  assert(!side.active.volatiles.MultiHit);
1444
1448
  assert(!foe.active.volatiles.MultiHit);
@@ -1459,7 +1463,7 @@ fn checkFaint(
1459
1463
  return .{ .p1 = foe_choice, .p2 = .Switch };
1460
1464
  }
1461
1465
 
1462
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1466
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1463
1467
  var side = battle.side(player);
1464
1468
  var foe = battle.foe(player);
1465
1469
  assert(side.stored().hp == 0);
@@ -1493,7 +1497,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1493
1497
  }
1494
1498
 
1495
1499
  try options.log.faint(.{ battle.active(player), done });
1496
- options.calc.capped(player);
1500
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1501
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1502
+ if (cap) options.calc.capped(player);
1497
1503
  return null;
1498
1504
  }
1499
1505
 
@@ -1813,7 +1819,6 @@ pub const Effects = struct {
1813
1819
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
1820
  if (!try checkHit(battle, player, move, options)) return;
1815
1821
  }
1816
- try options.chance.commit(player, .hit);
1817
1822
  }
1818
1823
 
1819
1824
  if (foe.active.volatiles.Confusion) return;
@@ -1846,10 +1851,7 @@ pub const Effects = struct {
1846
1851
  const foe_ident = battle.active(player.foe());
1847
1852
 
1848
1853
  // Pokémon Showdown handles hit/miss earlier in doMove
1849
- if (!showdown) {
1850
- if (!try checkHit(battle, player, move, options)) return null;
1851
- try options.chance.commit(player, .hit);
1852
- }
1854
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1853
1855
 
1854
1856
  if (volatiles.disable_move != 0) {
1855
1857
  try options.log.fail(.{ foe_ident, .None });
@@ -2051,7 +2053,6 @@ pub const Effects = struct {
2051
2053
  }
2052
2054
  }
2053
2055
 
2054
- try options.chance.commit(player, .hit);
2055
2056
  foe.active.volatiles.LeechSeed = true;
2056
2057
 
2057
2058
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2092,12 +2093,10 @@ pub const Effects = struct {
2092
2093
  if (!has_mimic) {
2093
2094
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2094
2095
  if (!try checkHit(battle, player, move, options)) return;
2095
- try options.chance.commit(player, .hit);
2096
2096
  return options.log.fail(.{ battle.active(player.foe()), .None });
2097
2097
  }
2098
2098
  }
2099
2099
  if (!try checkHit(battle, player, move, options)) return;
2100
- try options.chance.commit(player, .hit);
2101
2100
 
2102
2101
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2103
2102
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2151,7 +2150,6 @@ pub const Effects = struct {
2151
2150
  if (immune) return log.immune(.{ foe_ident, .None });
2152
2151
  if (!try checkHit(battle, player, move, options)) return;
2153
2152
  }
2154
- try options.chance.commit(player, .hit);
2155
2153
 
2156
2154
  foe_stored.status = Status.init(.PAR);
2157
2155
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2219,7 +2217,6 @@ pub const Effects = struct {
2219
2217
 
2220
2218
  if (move.effect == .Poison) {
2221
2219
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2222
- try options.chance.commit(player, .hit);
2223
2220
  } else {
2224
2221
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2225
2222
  return;
@@ -2288,9 +2285,8 @@ pub const Effects = struct {
2288
2285
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
2286
  });
2290
2287
  }
2291
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2288
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2292
2289
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2293
- try options.chance.commit(player, .hit);
2294
2290
  }
2295
2291
 
2296
2292
  // Sleep Clause Mod
@@ -2343,9 +2339,7 @@ pub const Effects = struct {
2343
2339
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2344
2340
  if (showdown) {
2345
2341
  if (battle.side(player).last_selected_move == .Teleport) return;
2346
- if (try checkHit(battle, player, move, options)) {
2347
- try options.chance.commit(player, .hit);
2348
- }
2342
+ _ = try checkHit(battle, player, move, options);
2349
2343
  battle.last_damage = 0;
2350
2344
  } else {
2351
2345
  try options.log.fail(.{ battle.active(player), .None });
@@ -2521,7 +2515,6 @@ pub const Effects = struct {
2521
2515
  } else {
2522
2516
  // checkHit already checks for Invulnerable
2523
2517
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2524
- try options.chance.commit(player, .hit);
2525
2518
  }
2526
2519
 
2527
2520
  var stats = &foe.active.stats;
@@ -2700,7 +2693,13 @@ pub const Rolls = struct {
2700
2693
  return p1;
2701
2694
  }
2702
2695
 
2703
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2696
+ fn criticalHit(
2697
+ battle: anytype,
2698
+ player: Player,
2699
+ rate: u8,
2700
+ spurious: bool,
2701
+ options: anytype,
2702
+ ) !bool {
2704
2703
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2705
2704
  val == .true
2706
2705
  else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
@@ -2710,7 +2709,7 @@ pub const Rolls = struct {
2710
2709
  else
2711
2710
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2712
2711
 
2713
- try options.chance.criticalHit(player, crit, rate);
2712
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2714
2713
  return crit;
2715
2714
  }
2716
2715
 
@@ -2732,17 +2731,19 @@ pub const Rolls = struct {
2732
2731
  return roll;
2733
2732
  }
2734
2733
 
2735
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2734
+ const uN = if (pkmn.options.miss) u8 else u9;
2735
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2736
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2736
2737
  const ok = if (options.calc.overridden(player, .hit)) |val|
2737
2738
  val == .true
2738
2739
  else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
- battle.rng.hit(player, accuracy)
2740
+ battle.rng.hit(player, acc)
2740
2741
  else if (showdown)
2741
- battle.rng.chance(u8, accuracy, 256)
2742
+ battle.rng.chance(uN, acc, 256)
2742
2743
  else
2743
- battle.rng.next() < accuracy;
2744
+ battle.rng.next() < acc;
2744
2745
 
2745
- options.chance.hit(ok, accuracy);
2746
+ try options.chance.hit(player, ok, acc);
2746
2747
  return ok;
2747
2748
  }
2748
2749
 
@@ -3068,7 +3069,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3068
3069
  const stored = side.stored();
3069
3070
 
3070
3071
  // While players are not given any input options on the cartridge in these cases,
3071
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3072
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3072
3073
  //
3073
3074
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3074
3075
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3076,7 +3077,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3076
3077
  // passing due to being forced into using a move. Instead of introducing another option
3077
3078
  // we simply repurpose Move with no move slot, even though pedantically this is not
3078
3079
  // strictly correct as the player would not have been presented the option to move or
3079
- // switch at all.
3080
+ // switch at all
3080
3081
  if (isForced(active)) {
3081
3082
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3082
3083
  n += 1;
@@ -3095,7 +3096,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3095
3096
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3096
3097
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3097
3098
  // either disables all other moves in the case of limited or requires you to select a
3098
- // move normally if sleeping/frozen/bound.
3099
+ // move normally if sleeping/frozen/bound
3099
3100
  if (!showdown and (limited or foe.active.volatiles.Binding or
3100
3101
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3101
3102
  {
@@ -3108,7 +3109,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3108
3109
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3109
3110
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3110
3111
  // Move will not result in forcing the subsequent use unless the user also had the
3111
- // proc-ed move in their moveset) and disabling all other moves.
3112
+ // proc-ed move in their moveset) and disabling all other moves
3112
3113
  if (limited) {
3113
3114
  assert(showdown);
3114
3115
  assert(side.last_selected_move != .None);
@@ -65,17 +65,47 @@ pub const Battle = struct {
65
65
  };
66
66
 
67
67
  pub fn prng(rand: *PSRNG) data.PRNG {
68
- // FIXME: what is max range in gen2?
69
68
  return .{
70
69
  .src = .{
71
70
  .seed = if (showdown)
72
71
  rand.newSeed()
73
- else
74
- .{
75
- rand.range(u8, 0, 256), rand.range(u8, 0, 256), rand.range(u8, 0, 256),
76
- rand.range(u8, 0, 256), rand.range(u8, 0, 256), rand.range(u8, 0, 256),
77
- rand.range(u8, 0, 256), rand.range(u8, 0, 256), rand.range(u8, 0, 256),
78
- },
72
+ else seed: {
73
+ // GLITCH: The initial RNG seed in Gen II link battles is heavily constrained
74
+ // https://www.smogon.com/forums/threads/rng-in-rby-link-battles.3746779
75
+ var seed = [_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0 };
76
+
77
+ // Bytes in seed can only range from 0-252, not 0-255
78
+ const max: u8 = 253;
79
+ // Instructions take multiples of 4 cycles to execute
80
+ var timer: u16 = 4 * rand.range(u16, 0, 16384);
81
+ var add: u8 = rand.range(u8, 0, 256);
82
+ var sub: u8 = rand.range(u8, 0, 256);
83
+ var carry: u8 = 0;
84
+ var div: u8 = 0;
85
+
86
+ var i: u8 = 0;
87
+ while (i < seed.len) {
88
+ div = @intCast(timer >> 8);
89
+ // This is effectively @addWithOverflow but with three operands
90
+ const result = @as(u16, add) + @as(u16, div) + @as(u16, carry);
91
+ add = @intCast(result & 255);
92
+ carry = @intFromBool(result > 255);
93
+ timer +%= 44;
94
+ div = @intCast(timer >> 8);
95
+ sub -%= div +% carry;
96
+
97
+ if (sub < max) {
98
+ seed[i] = sub;
99
+ i += 1;
100
+ timer +%= 152;
101
+ carry = 1;
102
+ } else {
103
+ timer +%= 132;
104
+ carry = 0;
105
+ }
106
+ }
107
+ break :seed seed;
108
+ },
79
109
  },
80
110
  };
81
111
  }
@@ -218,8 +218,9 @@
218
218
  border-radius: 0 4px 4px 0;
219
219
  }
220
220
  .moves ul {
221
- column-count: 2;
222
- column-gap: 2em;
221
+ display: grid;
222
+ grid-template-columns: repeat(2, auto);
223
+ gap: 0 2em;
223
224
  }
224
225
  .left {
225
226
  padding-right: 32px;
@@ -268,7 +269,7 @@
268
269
  }
269
270
  @media(max-width: 1200px) {
270
271
  .moves ul {
271
- column-count: 1;
272
+ grid-template-columns: repeat(1, auto);
272
273
  }
273
274
  }
274
275
  @media(max-width: 1100px) {
@@ -283,7 +284,7 @@
283
284
  margin-left: 5px;
284
285
  }
285
286
  .moves ul {
286
- column-count: 2;
287
+ grid-template-columns: repeat(2, auto);
287
288
  }
288
289
  .left {
289
290
  padding-right: 0;
@@ -309,7 +310,6 @@
309
310
  justify-content: center;
310
311
  }
311
312
  .moves ul {
312
- column-count: 1;
313
313
  display: inline-block;
314
314
  padding: 0;
315
315
  }