]> git.localhorst.tv Git - alttp.git/commitdiff
some guessing game fixes
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Mon, 18 Mar 2024 12:16:29 +0000 (13:16 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Mon, 18 Mar 2024 12:16:29 +0000 (13:16 +0100)
app/Http/Controllers/ChannelController.php
app/Models/Channel.php
app/TwitchBot/GuessingSolveCommand.php
resources/js/components/twitch-bot/GuessingSettingsForm.js
resources/js/i18n/de.js
resources/js/i18n/en.js
tests/Unit/Models/ChannelTest.php [new file with mode: 0644]

index dec17e90e7273f5d98cae2e6c0901c2bed0d7ee6..c47a4066a20a2d80112558498c1f198c9538786b 100644 (file)
@@ -164,18 +164,7 @@ class ChannelController extends Controller {
                        case 'solve':
                                if ($channel->hasActiveGuessing() && $channel->isValidGuess($validatedData['solution'])) {
                                        $winners = $channel->solveGuessing($validatedData['solution']);
-                                       $names = [];
-                                       foreach ($winners as $winner) {
-                                               if ($winner->score > 0) {
-                                                       $names[] = $winner->uname;
-                                               }
-                                       }
-                                       if (empty($names)) {
-                                               $msg = $channel->getGuessingSetting('no_winners_message');
-                                       } else {
-                                               $msg = $channel->getGuessingSetting('winners_message');
-                                               $msg = str_replace('{names}', $channel->listAnd($names), $msg);
-                                       }
+                                       $msg = $channel->listGuessingWinners($winners);
                                        if (!empty($msg)) {
                                                TwitchBotCommand::chat($channel->twitch_chat, $msg);
                                        }
@@ -250,7 +239,9 @@ class ChannelController extends Controller {
                $validatedData = $request->validate([
                        'active_message' => 'string',
                        'cancel_message' => 'string',
+                       'close_winners_message' => 'string',
                        'invalid_solution_message' => 'string',
+                       'leaderboard_type' => 'string',
                        'no_winners_message' => 'string',
                        'not_active_message' => 'string',
                        'points_exact_first' => 'numeric|min:1|max:5',
index 8debe4f9434ab554d70eb5e16086bf875ed1875e..bf3b98bc2b231c384a3079613e551514a7435c02 100644 (file)
@@ -32,7 +32,20 @@ class Channel extends Model {
        }
 
        public function getGuessingLeaderboard() {
-               return $this->winners()->selectRaw('(select t2.uname from guessing_winners t2 where t2.uid = guessing_winners.uid order by created_at desc limit 1) as name, sum(score) as score')->groupBy('uid')->orderBy('score', 'desc')->limit(10)->get();
+               $query = $this->winners()
+                       ->selectRaw('(select t2.uname from guessing_winners t2 where t2.uid = guessing_winners.uid order by created_at desc limit 1) as name, sum(score) as score')
+                       ->groupBy('uid')
+                       ->orderBy('score', 'desc')
+                       ->limit(10);
+               $type = $this->getGuessingSetting('leaderboard_type', 'all');
+               if ($type == 'month') {
+                       $query->where('created_at', '>=', now()->startOfMonth());
+               } else if ($type == 'year') {
+                       $query->where('created_at', '>=', now()->startOfYear());
+               } else if (is_numeric($type)) {
+                       $query->where('created_at', '>=', now()->sub($type, 'days'));
+               }
+               return $query->get();
        }
 
        public function hasActiveGuessing() {
@@ -119,23 +132,33 @@ class Channel extends Model {
                $this->save();
        }
 
+       public function transformGuess($original) {
+               $transformed = trim($original);
+               if ($this->guessing_type == 'gtbk') {
+                       $transformed = str_replace(['roodyo1Gtbigkey'], ['2'], $transformed);
+                       $transformed = str_ireplace(['vier 4Head'], ['4'], $transformed);
+               }
+               return $transformed;
+       }
+
        public function registerGuess($uid, $uname, $guess) {
                $model = new GuessingGuess();
                $model->channel()->associate($this);
                $model->uid = $uid;
                $model->uname = $uname;
-               $model->guess = $guess;
+               $model->guess = $this->transformGuess($guess);
                $model->save();
        }
 
        public function scoreGuessing($solution, $guess, $first) {
-               if ($guess == $solution) {
+               $transformed = $this->transformGuess($guess);
+               if ($transformed == $solution) {
                        if ($first) {
                                return $this->getGuessingSetting('points_exact_first', 1);
                        }
                        return $this->getGuessingSetting('points_exact_other', 1);
                }
-               $distance = abs(intval($guess) - intval($solution));
+               $distance = abs(intval($transformed) - intval($solution));
                if ($distance <= $this->getGuessingSetting('points_close_max', 3)) {
                        if ($first) {
                                return $this->getGuessingSetting('points_close_first', 1);
@@ -146,13 +169,33 @@ class Channel extends Model {
        }
 
        public function isValidGuess($solution) {
+               $transformed = $this->transformGuess($solution);
                if ($this->guessing_type == 'gtbk') {
-                       $int_solution = intval($solution);
-                       return $int_solution > 0 && $int_solution < 23;
+                       $int_solution = intval($transformed);
+                       return is_numeric($transformed) && $int_solution > 0 && $int_solution < 23;
                }
                return false;
        }
 
+       public function listGuessingWinners($winners) {
+               $names = [];
+               $distance = 0;
+               foreach ($winners as $winner) {
+                       if ($winner->score > 0) {
+                               $names[] = $winner->uname;
+                               $distance = abs(intval($winner->guess) - intval($winner->solution));
+                       }
+               }
+               $msg = '';
+               if (empty($names)) {
+                       $msg = $this->getGuessingSetting('no_winners_message');
+               } else {
+                       $msg = $this->getGuessingSetting($distance ? 'close_winners_message' : 'winners_message', $this->getGuessingSetting('winners_message'));
+                       $msg = str_replace(['{distance}', '{names}'], [$distance, $this->listAnd($names)], $msg);
+               }
+               return $msg;
+       }
+
        public function listAnd($entries) {
                $lang = empty($this->languages) ? 'en' : $this->languages[0];
                if ($lang == 'de') {
index b4a877d5b45d8b9378bb0cab431830872d46f6ec..e825791d374040d3c19b5bc641c403b55da8d818 100644 (file)
@@ -16,20 +16,8 @@ class GuessingSolveCommand extends ChatCommand {
                        return;
                }
                $winners = $this->channel->solveGuessing($args);
-               $names = [];
-               foreach ($winners as $winner) {
-                       if ($winner->score > 0) {
-                               $names[] = $winner->uname;
-                       }
-               }
-               if (empty($names)) {
-                       $msg = $this->channel->getGuessingSetting('no_winners_message');
-                       $this->messageChannel($msg);
-               } else {
-                       $msg = $this->channel->getGuessingSetting('winners_message');
-                       $msg = str_replace('{names}', $this->listAnd($names), $msg);
-                       $this->messageChannel($msg);
-               }
+               $msg = $this->channel->listGuessingWinners($winners);
+               $this->messageChannel($msg);
                $this->channel->clearGuessing();
        }
 
index 128d6f5d98b2b4eebca487df7af87db22e428e75..a465e4ea1e33e5e3c4cb528b167fc61ca5010ec8 100644 (file)
@@ -115,6 +115,27 @@ const GuessingSettingsForm = ({
                                        </Form.Control.Feedback>
                                : null}
                        </Form.Group>
+                       <Form.Group as={Col} controlId="gg.leaderboard_type" md={6}>
+                               <Form.Label>{t('twitchBot.guessingGame.leaderboardType')}</Form.Label>
+                               <Form.Select
+                                       isInvalid={!!(touched.leaderboard_type && errors.leaderboard_type)}
+                                       name="leaderboard_type"
+                                       onBlur={handleBlur}
+                                       onChange={handleChange}
+                                       value={values.leaderboard_type || 'all'}
+                               >
+                                       {['all', 'year', '365', 'month', '30'].map(type =>
+                                               <option key={type} value={type}>
+                                                       {t(`twitchBot.guessingGame.leaderboardTypes.${type}`)}
+                                               </option>
+                                       )}
+                               </Form.Select>
+                               {touched.leaderboard_type && errors.leaderboard_type ?
+                                       <Form.Control.Feedback type="invalid">
+                                               {t(errors.leaderboard_type)}
+                                       </Form.Control.Feedback>
+                               : null}
+                       </Form.Group>
                </Row>
                <Form.Group controlId="gg.start_message">
                        <Form.Label>{t('twitchBot.guessingGame.startMessage')}</Form.Label>
@@ -168,6 +189,26 @@ const GuessingSettingsForm = ({
                                </Form.Text>
                        }
                </Form.Group>
+               <Form.Group controlId="gg.close_winners_message">
+                       <Form.Label>{t('twitchBot.guessingGame.closeWinnersMessage')}</Form.Label>
+                       <Form.Control
+                               isInvalid={!!(touched.close_winners_message && errors.close_winners_message)}
+                               name="close_winners_message"
+                               onBlur={handleBlur}
+                               onChange={handleChange}
+                               type="text"
+                               value={values.close_winners_message || ''}
+                       />
+                       {touched.close_winners_message && errors.close_winners_message ?
+                               <Form.Control.Feedback type="invalid">
+                                       {t(errors.close_winners_message)}
+                               </Form.Control.Feedback>
+                       :
+                               <Form.Text muted>
+                                       {t('twitchBot.guessingGame.closeWinnersMessageHint')}
+                               </Form.Text>
+                       }
+               </Form.Group>
                <Form.Group controlId="gg.no_winners_message">
                        <Form.Label>{t('twitchBot.guessingGame.noWinnersMessage')}</Form.Label>
                        <Form.Control
@@ -265,7 +306,9 @@ GuessingSettingsForm.propTypes = {
        errors: PropTypes.shape({
                active_message: PropTypes.string,
                cancel_message: PropTypes.string,
+               close_winners_message: PropTypes.string,
                invalid_solution_message: PropTypes.string,
+               leaderboard_type: PropTypes.string,
                name: PropTypes.string,
                no_winners_message: PropTypes.string,
                not_active_message: PropTypes.string,
@@ -286,7 +329,9 @@ GuessingSettingsForm.propTypes = {
        touched: PropTypes.shape({
                active_message: PropTypes.bool,
                cancel_message: PropTypes.bool,
+               close_winners_message: PropTypes.bool,
                invalid_solution_message: PropTypes.bool,
+               leaderboard_type: PropTypes.bool,
                name: PropTypes.bool,
                no_winners_message: PropTypes.bool,
                not_active_message: PropTypes.bool,
@@ -302,7 +347,9 @@ GuessingSettingsForm.propTypes = {
        values: PropTypes.shape({
                active_message: PropTypes.string,
                cancel_message: PropTypes.string,
+               close_winners_message: PropTypes.string,
                invalid_solution_message: PropTypes.string,
+               leaderboard_type: PropTypes.string,
                name: PropTypes.string,
                no_winners_message: PropTypes.string,
                not_active_message: PropTypes.string,
@@ -339,8 +386,11 @@ export default withFormik({
                return {
                        active_message: getStringValue(settings, 'active_message', 'ActiveMessage'),
                        cancel_message: getStringValue(settings, 'cancel_message', 'CancelMessage'),
+                       close_winners_message:
+                               getStringValue(settings, 'close_winners_message', 'CloseWinnersMessage'),
                        invalid_solution_message:
                                getStringValue(settings, 'invalid_solution_message', 'InvalidSolutionMessage'),
+                       leaderboard_type: (settings && settings.leaderboard_type) || 'all',
                        name: name || '',
                        no_winners_message: getStringValue(settings, 'no_winners_message', 'NoWinnersMessage'),
                        not_active_message: getStringValue(settings, 'not_active_message', 'NotActiveMessage'),
@@ -357,6 +407,8 @@ export default withFormik({
        validationSchema: yup.object().shape({
                active_message: yup.string(),
                cancel_message: yup.string(),
+               close_winners_message: yup.string(),
+               leaderboard_type: yup.string(),
                invalid_solution_message: yup.string(),
                name: yup.string().required(),
                no_winners_message: yup.string(),
index 3cac15db0256f8381a8a5432b121ca5286d47b7b..0f20e4e8f350e9f7523c64243d6aa38f9f58b415 100644 (file)
@@ -541,8 +541,11 @@ export default {
                        guessingGame: {
                                activeMessage: 'Nachricht bei bereits laufendem Spiel',
                                cancelMessage: 'Nachricht bei Spielabbruch',
+                               closeWinnersMessage: 'Nachricht bei nicht-exakten Gewinnern',
+                               closeWinnersMessageHint: '{names} wird durch die Namen der Gewinner und {distance} durch den Abstand ersetzt',
                                defaultActiveMessage: 'Es läuft bereits ein Spiel auf diesem Kanal',
                                defaultCancelMessage: 'Spiel abgebrochen',
+                               defaultCloseWinnersMessage: 'Glückwunsch {names}! Nur {distance} daneben!',
                                defaultInvalidSolutionMessage: 'Bitte eine gültige Lösung für das Guessing Game angeben',
                                defaultNoWinnersMessage: 'keiner gewonnen :(',
                                defaultNotActiveMessage: 'Es läuft gerade kein Spiel auf diesem Kanal',
@@ -552,6 +555,14 @@ export default {
                                guesses: 'Tipps',
                                guessTimestamp: '{{ timestamp, LT }}',
                                invalidSolutionMessage: 'Nachricht bei ungültiger (oder fehlender) Lösung',
+                               leaderboardType: 'Leaderboard Style',
+                               leaderboardTypes: {
+                                       30: 'Laufender Monat',
+                                       365: 'Laufendes Jahr',
+                                       all: 'All Time',
+                                       month: 'Aktueller Monat',
+                                       year: 'Aktuelles Jahr',
+                               },
                                noWinnersMessage: 'Nachricht, falls keine Gewinner',
                                notActiveMessage: 'Nachricht, wenn kein Spiel läuft',
                                pointsCloseFirst: 'Punkte für den ersten nächsten Treffer',
index d9cd50ff574849bf311be4ad658b977a764ae770..4852f1458578623f553681c82ad974c4f0343a49 100644 (file)
@@ -541,8 +541,11 @@ export default {
                        guessingGame: {
                                activeMessage: 'Message when a game is already running',
                                cancelMessage: 'Game cancellation announcement',
+                               closeWinnersMessage: 'Message for inexact winners',
+                               closeWinnersMessageHint: '{names} will be replaced with a list of winners\' names and {distance} with the distance',
                                defaultActiveMessage: 'Channel already has an active guessing game',
                                defaultCancelMessage: 'Guessing game cancelled',
+                               defaultCloseWinnersMessage: 'Congrats {names} on guessing within {distance}!',
                                defaultInvalidSolutionMessage: 'Please provide a valid solution to the guessing game',
                                defaultNoWinnersMessage: 'nobody wins :(',
                                defaultNotActiveMessage: 'Channel has no active guessing game',
@@ -552,6 +555,14 @@ export default {
                                guesses: 'Guesses',
                                guessTimestamp: '{{ timestamp, LT }}',
                                invalidSolutionMessage: 'Message for invalid (or missing) solution',
+                               leaderboardType: 'Leaderboard type',
+                               leaderboardTypes: {
+                                       30: 'Running month',
+                                       365: 'Running year',
+                                       all: 'All time',
+                                       month: 'Current month',
+                                       year: 'Current year',
+                               },
                                noWinnersMessage: 'Announcement for no winners',
                                notActiveMessage: 'Message when no game is currently active',
                                pointsCloseFirst: 'Points for first close match',
diff --git a/tests/Unit/Models/ChannelTest.php b/tests/Unit/Models/ChannelTest.php
new file mode 100644 (file)
index 0000000..a5bbb34
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+
+namespace Tests\Unit\Models;
+
+use App\Models\Channel;
+use App\Models\GuessingWinner;
+use PHPUnit\Framework\TestCase;
+
+class ChannelTest extends TestCase {
+
+       public function test_guessing_solutions() {
+               $channel = new Channel();
+               $channel->guessing_type = 'gtbk';
+
+               $this->assertTrue($channel->isValidGuess('1'));
+               $this->assertTrue($channel->isValidGuess('22'));
+               $this->assertTrue($channel->isValidGuess('3 '));
+               $this->assertTrue($channel->isValidGuess('vier 4Head'));
+               $this->assertTrue($channel->isValidGuess('Vier 4Head'));
+               $this->assertTrue($channel->isValidGuess('roodyo1Gtbigkey'));
+
+               $this->assertFalse($channel->isValidGuess('1:22:56'));
+               $this->assertFalse($channel->isValidGuess('torch'));
+       }
+
+       public function test_guessing_scoring() {
+               $channel = new Channel();
+               $channel->guessing_type = 'gtbk';
+               $channel->guessing_settings = [
+                       'gtbk' => [
+                               'points_exact_first' => 5,
+                               'points_exact_other' => 4,
+                               'points_close_first' => 3,
+                               'points_close_other' => 2,
+                               'points_close_max' => 5,
+                       ],
+               ];
+
+               $this->assertEquals(5, $channel->scoreGuessing('3', '3', true));
+               $this->assertEquals(4, $channel->scoreGuessing('12', '12', false));
+               $this->assertEquals(3, $channel->scoreGuessing('15', '14', true));
+               $this->assertEquals(2, $channel->scoreGuessing('8', '6', false));
+               $this->assertEquals(0, $channel->scoreGuessing('7', '1', true));
+               $this->assertEquals(0, $channel->scoreGuessing('3', '22', false));
+
+               $this->assertEquals(5, $channel->scoreGuessing('2', 'roodyo1Gtbigkey', true));
+               $this->assertEquals(5, $channel->scoreGuessing('4', 'Vier 4Head', true));
+               $this->assertEquals(5, $channel->scoreGuessing('5', '5 ', true));
+       }
+
+       public function test_guessing_winners() {
+               $channel = new Channel();
+               $channel->guessing_type = 'gtbk';
+               $channel->guessing_settings = [
+                       'gtbk' => [
+                               'close_winners_message' => 'within {distance}: {names}',
+                               'no_winners_message' => 'no winners',
+                               'winners_message' => 'winners: {names}',
+                       ],
+               ];
+
+               $this->assertEquals('no winners', $channel->listGuessingWinners([]));
+
+               $winners = [];
+               $winner = new GuessingWinner();
+               $winner->uname = 'Horstie';
+               $winner->guess = '3';
+               $winner->solution = '3';
+               $winner->score = 1;
+               $winners[] = $winner;
+
+               $this->assertEquals('winners: Horstie', $channel->listGuessingWinners($winners));
+
+               $winner = new GuessingWinner();
+               $winner->uname = 'Borstie';
+               $winner->guess = '3';
+               $winner->solution = '3';
+               $winner->score = 1;
+               $winners[] = $winner;
+
+               $channel->languages = ['de'];
+               $this->assertEquals('winners: Horstie und Borstie', $channel->listGuessingWinners($winners));
+               $channel->languages = ['en'];
+               $this->assertEquals('winners: Horstie and Borstie', $channel->listGuessingWinners($winners));
+
+               $winners[0]->guess = '1';
+               $winners[1]->guess = '5';
+
+
+               $channel->languages = ['de'];
+               $this->assertEquals('within 2: Horstie und Borstie', $channel->listGuessingWinners($winners));
+               $channel->languages = ['en'];
+               $this->assertEquals('within 2: Horstie and Borstie', $channel->listGuessingWinners($winners));
+
+               $channel->guessing_settings = [
+                       'gtbk' => [
+                               'no_winners_message' => 'no winners',
+                               'winners_message' => 'winners: {names}',
+                       ],
+               ];
+
+               $channel->languages = ['de'];
+               $this->assertEquals('winners: Horstie und Borstie', $channel->listGuessingWinners($winners));
+               $channel->languages = ['en'];
+               $this->assertEquals('winners: Horstie and Borstie', $channel->listGuessingWinners($winners));
+       }
+
+}