Closed blackshadowshade closed 1 month ago
I've rebased this to be after the recent docker pull request.
I setup a dev site: https://pappy-and-bruno.blackshadowshade.dev.buttonweavers.com/
First smoke test, Pappy vs non-Bruno, failed: https://pappy-and-bruno.blackshadowshade.dev.buttonweavers.com/ui/game.html?game=258
(I got the internal error while accepting the game.)
The log message is:
Caught unexpected exception in ApiResponder: Infinite loop detected when advancing game state. Final game state: SPECIFY_RECIPES, referer: https://pappy-and-bruno.blackshadowshade.dev.buttonweavers.com/ui/game.html?game=258
Yay for a running test site!
I'll take a look and see what's triggering that infinite loop.
Oh wait, that's pretty obvious. The last two lines of code are not correctly in the if statement. That should be a doddle to fix.
Fix applied to the code. This should fix newly created games involving Bruno and Pappy, but not the already-broken game.
https://pappy-and-bruno.blackshadowshade.dev.buttonweavers.com/ui/ has been rebuild with the new code.
I've opened a Pappy vs Delia game at https://pappy-and-bruno.blackshadowshade.dev.buttonweavers.com/ui/game.html?game=258. It looks fine, I was able to choose swing values and play a turn.
Time to get testers looking at this.
I've logged into pappy-and-bruno.blackshadowshade.dev.buttonweavers.com . When I try to create a game of Pappy vs. Bruno I get the message "Internal error when calling createGame",
I was able to create a Bruno v. Pappy game but it won't load ("Internal error when calling loadGameData")
https://pappy-and-bruno.blackshadowshade.dev.buttonweavers.com/ui/game.html?game=260
I'm seeing the following error from the dev site when I try to create a Bruno vs Pappy game:
Notice: Undefined variable: message in /var/www/engine/BMGameAction.php on line 787
Notice: Undefined variable: message in /var/www/engine/BMGameAction.php on line 787
{"data":{"gameId":267},"message":"Game 267 created successfully.","status":"ok"}
That looks really odd, that shouldn't be failing.
Let me have a dig into the dev site, see if the correct code has been deployed.
Dev site has been rebuilt with rev3 --- try it out at https://pappy-and-bruno.blackshadowshade.dev.buttonweavers.com/ui/
All my tests work fine. Nice!
I'd be curious if Bruno vs. Random Button works correctly if the random button turns out to be Pappy. (And same in reverse.) I couldn't test for that case.
I'd be curious if Bruno vs. Random Button works correctly if the random button turns out to be Pappy.
That'll be easy enough to test once i have RandomAI testing working under ECS (which i think i have to do for this PR anyway, since it has logic changes).
I'm going to tear down this dev site --- it has pretty stale ECS code, and i'll get a copy of the database backup, so if we want to stand up another one, we'll be able to.
Okay. Time for some replay testing.
The button recipe for Bruno changed from B(8) B(8) B(20) B(20) B(X) to B(8) B(8) B(20) B(20) B(X) (X) because of Bruno's button special against Pappy
This message is hard to read. I think it's more usual for us to put square brackets around button recipes in log messages, so i would have expected this message to look like:
The button recipe for Bruno changed from [B(8) B(8) B(20) B(20) B(X)] to [B(8) B(8) B(20) B(20) B(X) (X)] because of Bruno's button special against Pappy
I think the latter is a little easier to parse.
I don't know if this is important, but it seems logically incorrect:
'originalRecipe' => '(4) (4) (10) B(20) (X) B(X)'
--- that is, originalRecipe
has the added B(X)
The button recipe for Pappy changed from (4) (4) (10) B(20) (X) to (4) (4) (10) B(20) (X) B(X) because of Pappy's button special against Bruno
Maybe i've forgotten what originalRecipe
does, but it seems to me that it should match the recipe as of the beginning of the action log. So if we have an action log entry talking about the recipe changing, then the recipe changed "during" the game, so originalRecipe
should contain the recipe as it was before that change.
What do you think?
I'll have to take a deep dive into the code to have a look at what's going on with originalRecipe and original_recipe. I'll let you know once I've taken a better look.
Here's a responder test that locks in the behavior as it is right now by instantiating and playing a couple of turns of Pappy vs Bruno. It doesn't have the brackets you just added, but it should be trivial to insert them.
Note that the diff contains both the new test in responder04Test.php
, and a little change you'll need to responderTestFramework.php
in order for games with Pappy or Bruno to be replayable.
diff --git a/test/src/api/responder04Test.php b/test/src/api/responder04Test.php
index a5b6f44f..55ec5383 100644
--- a/test/src/api/responder04Test.php
+++ b/test/src/api/responder04Test.php
@@ -939,5 +939,204 @@ class responder04Test extends responderTestFramework {
$retval = $this->verify_api_loadGameData($expData, $gameId, 10);
}
+
+ /**
+ * @depends responder00Test::test_request_savePlayerInfo
+ */
+ public function test_interface_game_00063() {
+
+ // responder003 is the POV player, so if you need to fake
+ // login as a different player e.g. to submit an attack, always
+ // return to responder003 as soon as you've done so
+ $this->game_number = 63;
+ $_SESSION = $this->mock_test_user_login('responder003');
+
+
+ $gameId = $this->verify_api_createGame(
+ array('bm_rand' => array(3, 3, 7, 11, 1, 3, 7, 7), 'bm_skill_rand' => array()),
+ 'responder003', 'responder004', 'Pappy', 'Bruno', 3,
+ '', NULL, 'gameId', array()
+ );
+
+ $expData = $this->generate_init_expected_data_array($gameId, 'responder003', 'responder004', 3, 'SPECIFY_DICE');
+ $expData['gameSkillsInfo'] = $this->get_skill_info(array('Berserk', 'Bruno', 'Pappy'));
+ $expData['playerDataArray'][0]['button'] = array('name' => 'Pappy', 'recipe' => '(4) (4) (10) B(20) (X) B(X)', 'originalRecipe' => '(4) (4) (10) B(20) (X) B(X)', 'artFilename' => 'pappy.png');
+ $expData['playerDataArray'][1]['button'] = array('name' => 'Bruno', 'recipe' => 'B(8) B(8) B(20) B(20) B(X) (X)', 'originalRecipe' => 'B(8) B(8) B(20) B(20) B(X) (X)', 'artFilename' => 'bruno.png');
+ $expData['playerDataArray'][0]['swingRequestArray'] = array('X' => array(4, 20));
+ $expData['playerDataArray'][1]['swingRequestArray'] = array('X' => array(4, 20));
+ $expData['playerDataArray'][0]['activeDieArray'] = array(
+ array('value' => NULL, 'sides' => 4, 'skills' => array(), 'properties' => array(), 'recipe' => '(4)', 'description' => '4-sided die'),
+ array('value' => NULL, 'sides' => 4, 'skills' => array(), 'properties' => array(), 'recipe' => '(4)', 'description' => '4-sided die'),
+ array('value' => NULL, 'sides' => 10, 'skills' => array(), 'properties' => array(), 'recipe' => '(10)', 'description' => '10-sided die'),
+ array('value' => NULL, 'sides' => 20, 'skills' => array('Berserk'), 'properties' => array(), 'recipe' => 'B(20)', 'description' => 'Berserk 20-sided die'),
+ array('value' => NULL, 'sides' => NULL, 'skills' => array(), 'properties' => array(), 'recipe' => '(X)', 'description' => 'X Swing Die'),
+ array('value' => NULL, 'sides' => NULL, 'skills' => array('Berserk'), 'properties' => array(), 'recipe' => 'B(X)', 'description' => 'Berserk X Swing Die'),
+ );
+ $expData['playerDataArray'][1]['activeDieArray'] = array(
+ array('value' => NULL, 'sides' => 8, 'skills' => array('Berserk'), 'properties' => array(), 'recipe' => 'B(8)', 'description' => 'Berserk 8-sided die'),
+ array('value' => NULL, 'sides' => 8, 'skills' => array('Berserk'), 'properties' => array(), 'recipe' => 'B(8)', 'description' => 'Berserk 8-sided die'),
+ array('value' => NULL, 'sides' => 20, 'skills' => array('Berserk'), 'properties' => array(), 'recipe' => 'B(20)', 'description' => 'Berserk 20-sided die'),
+ array('value' => NULL, 'sides' => 20, 'skills' => array('Berserk'), 'properties' => array(), 'recipe' => 'B(20)', 'description' => 'Berserk 20-sided die'),
+ array('value' => NULL, 'sides' => NULL, 'skills' => array('Berserk'), 'properties' => array(), 'recipe' => 'B(X)', 'description' => 'Berserk X Swing Die'),
+ array('value' => NULL, 'sides' => NULL, 'skills' => array(), 'properties' => array(), 'recipe' => '(X)', 'description' => 'X Swing Die'),
+ );
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => '', 'message' => 'The button recipe for Pappy changed from (4) (4) (10) B(20) (X) to (4) (4) (10) B(20) (X) B(X) because of Pappy\'s button special against Bruno'));
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => '', 'message' => 'The button recipe for Bruno changed from B(8) B(8) B(20) B(20) B(X) to B(8) B(8) B(20) B(20) B(X) (X) because of Bruno\'s button special against Pappy'));
+ $expData['gameActionLogCount'] = 3;
+
+ $expData['gameId'] = $gameId;
+ $expData['playerDataArray'][0]['playerId'] = $this->user_ids['responder003'];
+ $expData['playerDataArray'][1]['playerId'] = $this->user_ids['responder004'];
+
+ $retval = $this->verify_api_loadGameData($expData, $gameId, 10);
+
+ $this->verify_api_submitDieValues(
+ array(2, 1),
+ $gameId, 1, array('X' => 9), NULL);
+
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => 'responder003', 'message' => 'responder003 set die sizes'));
+ $expData['gameActionLogCount'] = 4;
+ $expData['playerDataArray'][0]['activeDieArray'][4]['description'] = "X Swing Die (with 9 sides)";
+ $expData['playerDataArray'][0]['activeDieArray'][4]['sides'] = 9;
+ $expData['playerDataArray'][0]['activeDieArray'][5]['description'] = "Berserk X Swing Die (with 9 sides)";
+ $expData['playerDataArray'][0]['activeDieArray'][5]['sides'] = 9;
+ $expData['playerDataArray'][0]['waitingOnAction'] = false;
+
+ $retval = $this->verify_api_loadGameData($expData, $gameId, 10);
+
+ $_SESSION = $this->mock_test_user_login('responder004');
+ $this->verify_api_submitDieValues(
+ array(10, 8),
+ $gameId, 1, array('X' => 14), NULL);
+
+ $_SESSION = $this->mock_test_user_login('responder003');
+ $expData['activePlayerIdx'] = 0;
+ $expData['gameActionLog'] = array();
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => '', 'message' => 'Game created by responder003'));
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => '', 'message' => 'The button recipe for Pappy changed from (4) (4) (10) B(20) (X) to (4) (4) (10) B(20) (X) B(X) because of Pappy\'s button special against Bruno'));
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => '', 'message' => 'The button recipe for Bruno changed from B(8) B(8) B(20) B(20) B(X) to B(8) B(8) B(20) B(20) B(X) (X) because of Bruno\'s button special against Pappy'));
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => 'responder003', 'message' => 'responder003 set swing values: X=9'));
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => 'responder004', 'message' => 'responder004 set swing values: X=14'));
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => '', 'message' => 'responder003 won initiative for round 1. Initial die values: responder003 rolled [(4):3, (4):3, (10):7, B(20):11, (X=9):2, B(X=9):1], responder004 rolled [B(8):1, B(8):3, B(20):7, B(20):7, B(X=14):10, (X=14):8].'));
+ $expData['gameActionLogCount'] = 6;
+ $expData['gameState'] = "START_TURN";
+ $expData['playerDataArray'][0]['activeDieArray'][0]['value'] = 3;
+ $expData['playerDataArray'][0]['activeDieArray'][1]['value'] = 3;
+ $expData['playerDataArray'][0]['activeDieArray'][2]['value'] = 7;
+ $expData['playerDataArray'][0]['activeDieArray'][3]['value'] = 11;
+ $expData['playerDataArray'][0]['activeDieArray'][4]['value'] = 2;
+ $expData['playerDataArray'][0]['activeDieArray'][5]['value'] = 1;
+ $expData['playerDataArray'][0]['roundScore'] = 28;
+ $expData['playerDataArray'][0]['sideScore'] = -9.3;
+ $expData['playerDataArray'][0]['swingRequestArray'] = array();
+ $expData['playerDataArray'][0]['waitingOnAction'] = true;
+ $expData['playerDataArray'][1]['activeDieArray'][0]['value'] = 1;
+ $expData['playerDataArray'][1]['activeDieArray'][1]['value'] = 3;
+ $expData['playerDataArray'][1]['activeDieArray'][2]['value'] = 7;
+ $expData['playerDataArray'][1]['activeDieArray'][3]['value'] = 7;
+ $expData['playerDataArray'][1]['activeDieArray'][4]['description'] = "Berserk X Swing Die (with 14 sides)";
+ $expData['playerDataArray'][1]['activeDieArray'][4]['sides'] = 14;
+ $expData['playerDataArray'][1]['activeDieArray'][4]['value'] = 10;
+ $expData['playerDataArray'][1]['activeDieArray'][5]['description'] = "X Swing Die (with 14 sides)";
+ $expData['playerDataArray'][1]['activeDieArray'][5]['sides'] = 14;
+ $expData['playerDataArray'][1]['activeDieArray'][5]['value'] = 8;
+ $expData['playerDataArray'][1]['roundScore'] = 42;
+ $expData['playerDataArray'][1]['sideScore'] = 9.3;
+ $expData['playerDataArray'][1]['swingRequestArray'] = array();
+ $expData['playerDataArray'][1]['waitingOnAction'] = false;
+ $expData['playerWithInitiativeIdx'] = 0;
+ $expData['validAttackTypeArray'] = array("Power", "Skill", "Berserk");
+
+ $retval = $this->verify_api_loadGameData($expData, $gameId, 10);
+
+ $this->verify_api_submitTurn(
+ array(1),
+ 'responder003 performed Power attack using [(X=9):2] against [B(8):1]; Defender B(8) was captured; Attacker (X=9) rerolled 2 => 1. ',
+ $retval, array(array(0, 4), array(1, 0)),
+ $gameId, 1, 'Power', 0, 1, '', array());
+
+ $expData['activePlayerIdx'] = 1;
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => 'responder003', 'message' => 'responder003 performed Power attack using [(X=9):2] against [B(8):1]; Defender B(8) was captured; Attacker (X=9) rerolled 2 => 1'));
+ $expData['gameActionLogCount'] = 7;
+ $expData['playerDataArray'][0]['activeDieArray'][4]['value'] = 1;
+ $expData['playerDataArray'][0]['capturedDieArray'][0]['properties'] = array("WasJustCaptured");
+ $expData['playerDataArray'][0]['capturedDieArray'][0]['recipe'] = "B(8)";
+ $expData['playerDataArray'][0]['capturedDieArray'][0]['sides'] = 8;
+ $expData['playerDataArray'][0]['capturedDieArray'][0]['value'] = 1;
+ $expData['playerDataArray'][0]['roundScore'] = 36;
+ $expData['playerDataArray'][0]['sideScore'] = -1.3;
+ $expData['playerDataArray'][0]['waitingOnAction'] = false;
+ $expData['playerDataArray'][1]['activeDieArray'][0]['value'] = 3;
+ $expData['playerDataArray'][1]['activeDieArray'][1]['description'] = "Berserk 20-sided die";
+ $expData['playerDataArray'][1]['activeDieArray'][1]['recipe'] = "B(20)";
+ $expData['playerDataArray'][1]['activeDieArray'][1]['sides'] = 20;
+ $expData['playerDataArray'][1]['activeDieArray'][1]['value'] = 7;
+ $expData['playerDataArray'][1]['activeDieArray'][3]['description'] = "Berserk X Swing Die (with 14 sides)";
+ $expData['playerDataArray'][1]['activeDieArray'][3]['recipe'] = "B(X)";
+ $expData['playerDataArray'][1]['activeDieArray'][3]['sides'] = 14;
+ $expData['playerDataArray'][1]['activeDieArray'][3]['value'] = 10;
+ $expData['playerDataArray'][1]['activeDieArray'][4]['description'] = "X Swing Die (with 14 sides)";
+ $expData['playerDataArray'][1]['activeDieArray'][4]['recipe'] = "(X)";
+ $expData['playerDataArray'][1]['activeDieArray'][4]['skills'] = array();
+ $expData['playerDataArray'][1]['activeDieArray'][4]['value'] = 8;
+ array_pop($expData['playerDataArray'][1]['activeDieArray']);
+ $expData['playerDataArray'][1]['roundScore'] = 38;
+ $expData['playerDataArray'][1]['sideScore'] = 1.3;
+ $expData['playerDataArray'][1]['waitingOnAction'] = true;
+ $expData['validAttackTypeArray'] = array("Power", "Berserk");
+
+ $retval = $this->verify_api_loadGameData($expData, $gameId, 10);
+
+ $_SESSION = $this->mock_test_user_login('responder004');
+ $this->verify_api_submitTurn(
+ array(4),
+ 'responder004 performed Berserk attack using [B(X=14):10] against [(4):3,(10):7]; Defender (4) was captured; Defender (10) was captured; Attacker B(X=14) changed size from 14 to 7 sides, recipe changed from B(X=14) to (X=7), rerolled 10 => 4. ',
+ $retval, array(array(1, 3), array(0, 0), array(0, 2)),
+ $gameId, 1, 'Berserk', 1, 0, '', array());
+
+ $_SESSION = $this->mock_test_user_login('responder003');
+ $expData['activePlayerIdx'] = 0;
+ array_unshift($expData['gameActionLog'], array('timestamp' => 'TIMESTAMP', 'player' => 'responder004', 'message' => 'responder004 performed Berserk attack using [B(X=14):10] against [(4):3,(10):7]; Defender (4) was captured; Defender (10) was captured; Attacker B(X=14) changed size from 14 to 7 sides, recipe changed from B(X=14) to (X=7), rerolled 10 => 4'));
+ $expData['gameActionLogCount'] = 8;
+ $expData['playerDataArray'][0]['activeDieArray'][1]['description'] = "Berserk 20-sided die";
+ $expData['playerDataArray'][0]['activeDieArray'][1]['recipe'] = "B(20)";
+ $expData['playerDataArray'][0]['activeDieArray'][1]['sides'] = 20;
+ $expData['playerDataArray'][0]['activeDieArray'][1]['skills'] = array("Berserk");
+ $expData['playerDataArray'][0]['activeDieArray'][1]['value'] = 11;
+ $expData['playerDataArray'][0]['activeDieArray'][2]['description'] = "X Swing Die (with 9 sides)";
+ $expData['playerDataArray'][0]['activeDieArray'][2]['recipe'] = "(X)";
+ $expData['playerDataArray'][0]['activeDieArray'][2]['sides'] = 9;
+ $expData['playerDataArray'][0]['activeDieArray'][2]['value'] = 1;
+ $expData['playerDataArray'][0]['activeDieArray'][3]['description'] = "Berserk X Swing Die (with 9 sides)";
+ $expData['playerDataArray'][0]['activeDieArray'][3]['recipe'] = "B(X)";
+ $expData['playerDataArray'][0]['activeDieArray'][3]['sides'] = 9;
+ $expData['playerDataArray'][0]['activeDieArray'][3]['value'] = 1;
+ array_pop($expData['playerDataArray'][0]['activeDieArray']);
+ array_pop($expData['playerDataArray'][0]['activeDieArray']);
+ $expData['playerDataArray'][0]['capturedDieArray'][0]['properties'] = array();
+ $expData['playerDataArray'][0]['roundScore'] = 29;
+ $expData['playerDataArray'][0]['sideScore'] = -13;
+ $expData['playerDataArray'][0]['waitingOnAction'] = true;
+ $expData['playerDataArray'][1]['activeDieArray'][3]['description'] = "X Swing Die (with 7 sides)";
+ $expData['playerDataArray'][1]['activeDieArray'][3]['properties'] = array("HasJustSplit", "JustPerformedBerserkAttack");
+ $expData['playerDataArray'][1]['activeDieArray'][3]['recipe'] = "(X)";
+ $expData['playerDataArray'][1]['activeDieArray'][3]['sides'] = 7;
+ $expData['playerDataArray'][1]['activeDieArray'][3]['skills'] = array();
+ $expData['playerDataArray'][1]['activeDieArray'][3]['value'] = 4;
+ $expData['playerDataArray'][1]['capturedDieArray'][0]['properties'] = array("WasJustCaptured");
+ $expData['playerDataArray'][1]['capturedDieArray'][0]['recipe'] = "(4)";
+ $expData['playerDataArray'][1]['capturedDieArray'][0]['sides'] = 4;
+ $expData['playerDataArray'][1]['capturedDieArray'][0]['value'] = 3;
+ $expData['playerDataArray'][1]['capturedDieArray'][1]['properties'] = array("WasJustCaptured");
+ $expData['playerDataArray'][1]['capturedDieArray'][1]['recipe'] = "(10)";
+ $expData['playerDataArray'][1]['capturedDieArray'][1]['sides'] = 10;
+ $expData['playerDataArray'][1]['capturedDieArray'][1]['value'] = 7;
+ $expData['playerDataArray'][1]['roundScore'] = 48.5;
+ $expData['playerDataArray'][1]['sideScore'] = 13;
+ $expData['playerDataArray'][1]['waitingOnAction'] = false;
+ $expData['validAttackTypeArray'] = array("Power", "Skill", "Berserk");
+
+ $retval = $this->verify_api_loadGameData($expData, $gameId, 10);
+ }
}
diff --git a/test/src/api/responderTestFramework.php b/test/src/api/responderTestFramework.php
index 616f2de6..e15817b0 100644
--- a/test/src/api/responderTestFramework.php
+++ b/test/src/api/responderTestFramework.php
@@ -132,6 +132,11 @@ class responderTestFramework extends PHPUnit_Framework_TestCase {
'Stealth' => 'Stealth dice may be targeted by boom attacks',
),
),
+ 'Bruno' => array(
+ 'code' => '',
+ 'description' => 'Bruno gets an extra die, an (X), if his opponent is Pappy.',
+ 'interacts' => array(),
+ ),
'Chance' => array(
'code' => 'c',
'description' => 'If you do not have the initiative at the start of a round you may re-roll one of your Chance Dice. If this results in you gaining the initiative, your opponent may re-roll one of their Chance Dice. This can continue with each player re-rolling Chance Dice, even re-rolling the same die, until one person fails to gain the initiative or lets their opponent go first. Re-rolling Chance Dice is not only a way to gain the initiative; it can also be useful in protecting your larger dice, or otherwise improving your starting roll. Unlike Focus Dice, Chance Dice can be immediately re-used in an attack even if you do gain the initiative with them.',
@@ -281,6 +286,11 @@ class responderTestFramework extends PHPUnit_Framework_TestCase {
'Mood' => 'Dice with both Ornery and Mood Swing have their sizes randomized during ornery rerolls',
),
),
+ 'Pappy' => array(
+ 'code' => '',
+ 'description' => 'Pappy gets an extra die, a B(X), if his opponent is Bruno.',
+ 'interacts' => array(),
+ ),
'Poison' => array(
'code' => 'p',
'description' => 'These dice are worth negative points. If you keep a Poison Die of your own at the end of a round, subtract its full value from your score. If you capture a Poison Die from someone else, subtract half its value from your score.',
Sounds good. I think this is on your plate now to decide what you think is right about originalRecipe, and install the replay test. I'm going to tear the replay site down for now --- i didn't see any other issues except the missing responderTestFramework
skill info stubs.
I've taken a look, and I believe that you're correct, the original recipe needs to be cached before the button skill takes effect. Let's see if I can work out how to do that!
Okay, I've shifted the logic of the recipe change to being slightly later so that the cache occurs first. I've also added the responder test and rebased.
I think this is ready to be redeployed to a test site so that testers can take one more quick look.
I kicked off replay testing, and started launching a dev site. It should be up in about 15 minutes at https://pappy-and-bruno.blackshadowshade.dev.buttonweavers.com/ (if it doesn't come up, i'll take a look tomorrow).
I've taken a look at the test site and a Pappy vs Bruno game appears to work okay into round 2.
@craw-daddy, @dwvanstone, if one of you (or another tester) could take a quick look at this test site, that would be lovely.
I've explored the test site, looked at the button sets, created a few games, and everything looks good to me.
Thanks, all! I'll let the replay tests run for a few more hours, and assuming there are no surprises when i check this evening or so, i'll merge this.
I think everything checks out here. (The test site played 1600 new and replayed games without incident.)
Supersedes #2905, partly addresses #372.
I've taken the pull request from @craw-daddy and added action logging.
To do: