]> git.localhorst.tv Git - alttp.git/commitdiff
simple guessing game
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 28 Feb 2024 15:16:37 +0000 (16:16 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 28 Feb 2024 15:29:07 +0000 (16:29 +0100)
14 files changed:
app/Models/Channel.php
app/Models/GuessingGuess.php [new file with mode: 0644]
app/Models/GuessingWinner.php [new file with mode: 0644]
app/TwitchBot/ChatCommand.php
app/TwitchBot/GuessingCancelCommand.php [new file with mode: 0644]
app/TwitchBot/GuessingSolveCommand.php [new file with mode: 0644]
app/TwitchBot/GuessingStartCommand.php [new file with mode: 0644]
app/TwitchBot/GuessingStopCommand.php [new file with mode: 0644]
app/TwitchBot/IRCMessage.php
app/TwitchBot/TwitchAppBot.php
database/migrations/2023_02_20_093249_create_channel_episode_table.php
database/migrations/2024_02_28_102210_channel_guessing_game.php [new file with mode: 0644]
database/migrations/2024_02_28_120923_create_guessing_guesses_table.php [new file with mode: 0644]
database/migrations/2024_02_28_141025_create_guessing_winners_table.php [new file with mode: 0644]

index d1f0fdc784d4f4e0f4078be186322b4a54901bd8..32ac875bb707c0760f9e75d2362a64416db7da61 100644 (file)
@@ -16,6 +16,97 @@ class Channel extends Model
                        ->first();
        }
 
+       public function hasActiveGuessing() {
+               return !is_null($this->guessing_start);
+       }
+
+       public function isAcceptingGuesses() {
+               return !is_null($this->guessing_start) && is_null($this->guessing_end);
+       }
+
+       public function startGuessing($type) {
+               $this->guessing_type = $type;
+               $this->guessing_start = now();
+               $this->save();
+       }
+
+       public function stopGuessing() {
+               $this->guessing_end = now();
+               $this->save();
+       }
+
+       public function solveGuessing($solution) {
+               $start = $this->guessing_start;
+               $end = is_null($this->guessing_end) ? now() : $this->guessing_end;
+               $guesses = $this->guesses()->whereBetween('created_at', [$start, $end])->orderBy('created_at', 'ASC')->get();
+               $unique_guesses = [];
+               foreach ($guesses as $guess) {
+                       $unique_guesses[$guess->uid] = $guess;
+               }
+               $candidates = [];
+               foreach ($unique_guesses as $guess) {
+                       if ($guess->guess == $solution) {
+                               $candidates[] = $guess;
+                       }
+               }
+               if (empty($candidates) && is_numeric($solution)) {
+                       $min_distance = null;
+                       foreach ($unique_guesses as $guess) {
+                               $distance = abs(intval($guess->guess) - intval($solution));
+                               if (is_null($min_distance) || $distance == $min_distance) {
+                                       $candidates[] = $guess;
+                               } else if ($distance < $min_distance) {
+                                       $candidates = [$guess];
+                                       $min_distance = $distance;
+                               }
+                       }
+               }
+               $winners = [];
+               $first = true;
+               foreach ($candidates as $candidate) {
+                       $score = $this->scoreGuessing($solution, $candidate->guess, $first);
+                       $winner = new GuessingWinner();
+                       $winner->channel()->associate($this);
+                       $winner->pod = $start;
+                       $winner->uid = $candidate->uid;
+                       $winner->uname = $candidate->uname;
+                       $winner->guess = $candidate->guess;
+                       $winner->solution = $solution;
+                       $winner->score = $score;
+                       $winner->save();
+                       $winners[] = $winner;
+                       $first = false;
+               }
+               return $winners;
+       }
+
+       public function clearGuessing() {
+               $this->guessing_start = null;
+               $this->guessing_end = null;
+               $this->save();
+       }
+
+       public function registerGuess($uid, $uname, $guess) {
+               $model = new GuessingGuess();
+               $model->channel()->associate($this);
+               $model->uid = $uid;
+               $model->uname = $uname;
+               $model->guess = $guess;
+               $model->save();
+       }
+
+       public function scoreGuessing($solution, $guess, $first) {
+               return 1;
+       }
+
+       public function isValidGuess($solution) {
+               if ($this->guessing_type == 'gtbk') {
+                       $int_solution = intval($solution);
+                       return $int_solution > 0 && $int_solution < 23;
+               }
+               return false;
+       }
+
        public function crews() {
                return $this->hasMany(ChannelCrew::class);
        }
@@ -26,6 +117,10 @@ class Channel extends Model
                        ->withPivot('accept_comms', 'accept_tracker');
        }
 
+       public function guesses() {
+               return $this->hasMany(GuessingGuess::class);
+       }
+
        public function organization() {
                return $this->belongsTo(Organization::class);
        }
@@ -34,6 +129,8 @@ class Channel extends Model
                'chat' => 'boolean',
                'chat_commands' => 'array',
                'chat_settings' => 'array',
+               'guessing_start' => 'datetime',
+               'guessing_end' => 'datetime',
                'languages' => 'array',
                'join' => 'boolean',
        ];
diff --git a/app/Models/GuessingGuess.php b/app/Models/GuessingGuess.php
new file mode 100644 (file)
index 0000000..12cff87
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class GuessingGuess extends Model {
+
+       use HasFactory;
+
+       public function channel() {
+               return $this->belongsTo(Channel::class);
+       }
+
+}
diff --git a/app/Models/GuessingWinner.php b/app/Models/GuessingWinner.php
new file mode 100644 (file)
index 0000000..0bbdf00
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class GuessingWinner extends Model {
+
+       use HasFactory;
+
+       public function channel() {
+               return $this->belongsTo(Channel::class);
+       }
+
+}
index feea8abf92386c0a6cd1fa1cee66bd55dfeb95c0..d03f7829e2704923f3b72780a8b3f1fd12596fb2 100644 (file)
@@ -3,6 +3,7 @@
 namespace App\TwitchBot;
 
 use App\Models\Channel;
+use Illuminate\Support\Arr;
 
 abstract class ChatCommand {
 
@@ -12,6 +13,18 @@ abstract class ChatCommand {
                        case 'crew':
                                $cmd = new CrewCommand();
                                break;
+                       case 'guessing-cancel':
+                               $cmd = new GuessingCancelCommand();
+                               break;
+                       case 'guessing-solve':
+                               $cmd = new GuessingSolveCommand();
+                               break;
+                       case 'guessing-start':
+                               $cmd = new GuessingStartCommand();
+                               break;
+                       case 'guessing-stop':
+                               $cmd = new GuessingStopCommand();
+                               break;
                        case 'runner':
                                $cmd = new RunnerCommand();
                                break;
@@ -24,12 +37,31 @@ abstract class ChatCommand {
                return $cmd;
        }
 
+       public function checkAccess(IRCMessage $msg) {
+               $restrict = $this->getStringConfig('restrict', 'none');
+               if ($restrict == 'owner') {
+                       return $msg->isOwner();
+               }
+               if ($restrict == 'mod') {
+                       return $msg->isMod();
+               }
+               return true;
+       }
+
        public abstract function execute($args);
 
        protected function getBooleanConfig($name, $default = false) {
                return array_key_exists($name, $this->config) ? $this->config[$name] : $default;
        }
 
+       protected function getStringConfig($name, $default = '') {
+               return array_key_exists($name, $this->config) ? $this->config[$name] : $default;
+       }
+
+       protected function listAnd($entries) {
+               return Arr::join($entries, ', ', ' and ');
+       }
+
        protected function messageChannel($str) {
                $msg = IRCMessage::privmsg($this->channel->twitch_chat, $str);
                $this->bot->sendIRCMessage($msg);
diff --git a/app/TwitchBot/GuessingCancelCommand.php b/app/TwitchBot/GuessingCancelCommand.php
new file mode 100644 (file)
index 0000000..c510a32
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+namespace App\TwitchBot;
+
+class GuessingCancelCommand extends ChatCommand {
+
+       public function execute($args) {
+               if ($this->chanel->hasActiveGuessing()) {
+                       $this->channel->clearGuessing();
+                       $this->messageChannel('Guessing game cancelled');
+               }
+       }
+
+}
+
+?>
diff --git a/app/TwitchBot/GuessingSolveCommand.php b/app/TwitchBot/GuessingSolveCommand.php
new file mode 100644 (file)
index 0000000..57ee5f8
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+namespace App\TwitchBot;
+
+class GuessingSolveCommand extends ChatCommand {
+
+       public function execute($args) {
+               if (!$this->channel->hasActiveGuessing()) {
+                       $this->messageChannel('Channel has no active guessing game');
+                       return;
+               }
+               if (empty($args)) {
+                       $this->messageChannel('Please provide a solution to the guessing game');
+                       return;
+               }
+               if (!$this->channel->isValidGuess($args)) {
+                       $this->messageChannel('Please provide a valid solution to the guessing game');
+                       return;
+               }
+               $winners = $this->channel->solveGuessing($args);
+               $names = [];
+               foreach ($winners as $winner) {
+                       if ($winner->score > 0) {
+                               $names[] = $winner->uname;
+                       }
+               }
+               if (empty($names)) {
+                       $this->messageChannel('nobody wins :(');
+               } else {
+                       $this->messageChannel('Congrats '.$this->listAnd($names));
+               }
+               $this->channel->clearGuessing();
+       }
+
+}
+
+?>
diff --git a/app/TwitchBot/GuessingStartCommand.php b/app/TwitchBot/GuessingStartCommand.php
new file mode 100644 (file)
index 0000000..d2b2168
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+namespace App\TwitchBot;
+
+class GuessingStartCommand extends ChatCommand {
+
+       public function execute($args) {
+               if ($this->channel->hasActiveGuessing()) {
+                       $this->messageChannel('Channel already has an active guessing game');
+                       return;
+               }
+               $type = $this->getStringConfig('type', 'gtbk');
+               $this->channel->startGuessing($type);
+               $this->messageChannel('Get your guesses in');
+       }
+
+}
+
+?>
diff --git a/app/TwitchBot/GuessingStopCommand.php b/app/TwitchBot/GuessingStopCommand.php
new file mode 100644 (file)
index 0000000..cc4cd08
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+namespace App\TwitchBot;
+
+class GuessingStopCommand extends ChatCommand {
+
+       public function execute($args) {
+               if (!$this->channel->hasActiveGuessing()) {
+                       $this->messageChannel('Channel has no active guessing game');
+                       return;
+               }
+               $this->channel->stopGuessing();
+               $this->messageChannel('Guessing closed');
+       }
+
+}
+
+?>
index 1e64bd9b6de7c12c0674ce7966e06adbd914f0b0..4fb54c670b7a8890e89af029f6dc9b649efb42cc 100644 (file)
@@ -195,6 +195,14 @@ class IRCMessage {
                return $this->command == 'PRIVMSG';
        }
 
+       public function isOwner() {
+               return substr($this->getPrivMsgTarget(), 1) == $this->nick;
+       }
+
+       public function isMod() {
+               return $this->isOwner() || (isset($this->tags['mod']) && $this->tags['mod'] = '1');
+       }
+
        public function makePong() {
                $msg = new IRCMessage();
                $msg->command = 'PONG';
index ce0f75112558563f9cf7f8e7866082e50bc850e9..04d03e609cb6ecd007df3927d467e25de89b249f 100644 (file)
@@ -18,11 +18,25 @@ class TwitchAppBot extends TwitchBot {
        public function handlePrivMsg(IRCMessage $msg) {
                $target = $msg->getPrivMsgTarget();
                if ($target[0] != '#') return; // direct message
-               $text = $msg->getText();
-               if ($text[0] != '!') return;
                $channel = $this->getMessageChannel($msg);
                if (!$channel) return;
-               $this->handleChatCommand($channel, $msg);
+               $text = $msg->getText();
+               if ($text[0] == '!') {
+                       $this->handleChatCommand($channel, $msg);
+               } else if (
+                       $channel->hasActiveGuessing() &&
+                       !empty($msg->tags['user-id']) &&
+                       !empty($msg->tags['display-name'] &&
+                       $channel->isValidGuess($text))
+               ) {
+                       $uid = 't:'.$msg->tags['user-id'];
+                       $uname = $msg->tags['display-name'];
+                       try {
+                               $channel->registerGuess($uid, $uname, $text);
+                       } catch (\Exception $e) {
+                               $this->getLogger()->warning('error registering guess "'.$text.'" on channel '.$channel->title.': '.$e->getMessage());
+                       }
+               }
        }
 
        public function handleChatCommand(Channel $channel, IRCMessage $msg) {
@@ -32,7 +46,9 @@ class TwitchAppBot extends TwitchBot {
                $this->getLogger()->info('got command '.$cmd[0].' on channel '.$channel->title);
                try {
                        $command = ChatCommand::create($this, $channel, $config);
-                       $command->execute($cmd[1] ?? '');
+                       if ($command->checkAccess($msg)) {
+                               $command->execute($cmd[1] ?? '');
+                       }
                } catch (\Exception $e) {
                        $this->getLogger()->warning('error executing command '.$cmd[0].' on channel '.$channel->title.': '.$e->getMessage());
                }
index 7f337344de630cd2ab29bbacf898ca420b463458..103e6c939fe7d207e63c18e0da344440a36cb08e 100644 (file)
@@ -6,27 +6,27 @@ use Illuminate\Support\Facades\Schema;
 
 return new class extends Migration
 {
-    /**
-     * Run the migrations.
-     *
-     * @return void
-     */
-    public function up()
-    {
-        Schema::create('channel_episode', function (Blueprint $table) {
-            $table->id();
+       /**
+        * Run the migrations.
+        *
+        * @return void
+        */
+       public function up()
+       {
+               Schema::create('channel_episode', function (Blueprint $table) {
+                       $table->id();
                        $table->foreignId('channel_id')->constrained();
                        $table->foreignId('episode_id')->constrained();
-        });
-    }
+               });
+       }
 
-    /**
-     * Reverse the migrations.
-     *
-     * @return void
-     */
-    public function down()
-    {
-        Schema::dropIfExists('channel_episode');
-    }
+       /**
+        * Reverse the migrations.
+        *
+        * @return void
+        */
+       public function down()
+       {
+               Schema::dropIfExists('channel_episode');
+       }
 };
diff --git a/database/migrations/2024_02_28_102210_channel_guessing_game.php b/database/migrations/2024_02_28_102210_channel_guessing_game.php
new file mode 100644 (file)
index 0000000..28fb7ec
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        *
+        * @return void
+        */
+       public function up()
+       {
+               Schema::table('channels', function (Blueprint $table) {
+                       $table->string('guessing_type')->default('');
+                       $table->datetime('guessing_start')->nullable()->default(null);
+                       $table->datetime('guessing_end')->nullable()->default(null);
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        *
+        * @return void
+        */
+       public function down()
+       {
+               Schema::table('channels', function (Blueprint $table) {
+                       $table->dropColumn('guessing_type');
+                       $table->dropColumn('guessing_start');
+                       $table->dropColumn('guessing_end');
+               });
+       }
+};
diff --git a/database/migrations/2024_02_28_120923_create_guessing_guesses_table.php b/database/migrations/2024_02_28_120923_create_guessing_guesses_table.php
new file mode 100644 (file)
index 0000000..eecd972
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        *
+        * @return void
+        */
+       public function up()
+       {
+               Schema::create('guessing_guesses', function (Blueprint $table) {
+                       $table->id();
+                       $table->timestamps();
+                       $table->foreignId('channel_id')->constrained();
+                       $table->string('uid');
+                       $table->string('uname');
+                       $table->string('guess');
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        *
+        * @return void
+        */
+       public function down()
+       {
+               Schema::dropIfExists('guessing_guesses');
+       }
+};
diff --git a/database/migrations/2024_02_28_141025_create_guessing_winners_table.php b/database/migrations/2024_02_28_141025_create_guessing_winners_table.php
new file mode 100644 (file)
index 0000000..48681c1
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        *
+        * @return void
+        */
+       public function up()
+       {
+               Schema::create('guessing_winners', function (Blueprint $table) {
+                       $table->id();
+                       $table->timestamps();
+                       $table->foreignId('channel_id')->constrained();
+                       $table->string('pod');
+                       $table->string('uid');
+                       $table->string('uname');
+                       $table->string('guess');
+                       $table->string('solution');
+                       $table->integer('score');
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        *
+        * @return void
+        */
+       public function down()
+       {
+               Schema::dropIfExists('guessing_winners');
+       }
+};