From 7fc357a5943bf280ce2fa9aa97ec516af61efd69 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Wed, 28 Feb 2024 16:16:37 +0100 Subject: [PATCH] simple guessing game --- app/Models/Channel.php | 97 +++++++++++++++++++ app/Models/GuessingGuess.php | 16 +++ app/Models/GuessingWinner.php | 16 +++ app/TwitchBot/ChatCommand.php | 32 ++++++ app/TwitchBot/GuessingCancelCommand.php | 16 +++ app/TwitchBot/GuessingSolveCommand.php | 37 +++++++ app/TwitchBot/GuessingStartCommand.php | 19 ++++ app/TwitchBot/GuessingStopCommand.php | 18 ++++ app/TwitchBot/IRCMessage.php | 8 ++ app/TwitchBot/TwitchAppBot.php | 24 ++++- ...20_093249_create_channel_episode_table.php | 40 ++++---- ...024_02_28_102210_channel_guessing_game.php | 36 +++++++ ...8_120923_create_guessing_guesses_table.php | 35 +++++++ ...8_141025_create_guessing_winners_table.php | 38 ++++++++ 14 files changed, 408 insertions(+), 24 deletions(-) create mode 100644 app/Models/GuessingGuess.php create mode 100644 app/Models/GuessingWinner.php create mode 100644 app/TwitchBot/GuessingCancelCommand.php create mode 100644 app/TwitchBot/GuessingSolveCommand.php create mode 100644 app/TwitchBot/GuessingStartCommand.php create mode 100644 app/TwitchBot/GuessingStopCommand.php create mode 100644 database/migrations/2024_02_28_102210_channel_guessing_game.php create mode 100644 database/migrations/2024_02_28_120923_create_guessing_guesses_table.php create mode 100644 database/migrations/2024_02_28_141025_create_guessing_winners_table.php diff --git a/app/Models/Channel.php b/app/Models/Channel.php index d1f0fdc..32ac875 100644 --- a/app/Models/Channel.php +++ b/app/Models/Channel.php @@ -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 index 0000000..12cff87 --- /dev/null +++ b/app/Models/GuessingGuess.php @@ -0,0 +1,16 @@ +belongsTo(Channel::class); + } + +} diff --git a/app/Models/GuessingWinner.php b/app/Models/GuessingWinner.php new file mode 100644 index 0000000..0bbdf00 --- /dev/null +++ b/app/Models/GuessingWinner.php @@ -0,0 +1,16 @@ +belongsTo(Channel::class); + } + +} diff --git a/app/TwitchBot/ChatCommand.php b/app/TwitchBot/ChatCommand.php index feea8ab..d03f782 100644 --- a/app/TwitchBot/ChatCommand.php +++ b/app/TwitchBot/ChatCommand.php @@ -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 index 0000000..c510a32 --- /dev/null +++ b/app/TwitchBot/GuessingCancelCommand.php @@ -0,0 +1,16 @@ +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 index 0000000..57ee5f8 --- /dev/null +++ b/app/TwitchBot/GuessingSolveCommand.php @@ -0,0 +1,37 @@ +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 index 0000000..d2b2168 --- /dev/null +++ b/app/TwitchBot/GuessingStartCommand.php @@ -0,0 +1,19 @@ +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 index 0000000..cc4cd08 --- /dev/null +++ b/app/TwitchBot/GuessingStopCommand.php @@ -0,0 +1,18 @@ +channel->hasActiveGuessing()) { + $this->messageChannel('Channel has no active guessing game'); + return; + } + $this->channel->stopGuessing(); + $this->messageChannel('Guessing closed'); + } + +} + +?> diff --git a/app/TwitchBot/IRCMessage.php b/app/TwitchBot/IRCMessage.php index 1e64bd9..4fb54c6 100644 --- a/app/TwitchBot/IRCMessage.php +++ b/app/TwitchBot/IRCMessage.php @@ -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'; diff --git a/app/TwitchBot/TwitchAppBot.php b/app/TwitchBot/TwitchAppBot.php index ce0f751..04d03e6 100644 --- a/app/TwitchBot/TwitchAppBot.php +++ b/app/TwitchBot/TwitchAppBot.php @@ -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()); } diff --git a/database/migrations/2023_02_20_093249_create_channel_episode_table.php b/database/migrations/2023_02_20_093249_create_channel_episode_table.php index 7f33734..103e6c9 100644 --- a/database/migrations/2023_02_20_093249_create_channel_episode_table.php +++ b/database/migrations/2023_02_20_093249_create_channel_episode_table.php @@ -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 index 0000000..28fb7ec --- /dev/null +++ b/database/migrations/2024_02_28_102210_channel_guessing_game.php @@ -0,0 +1,36 @@ +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 index 0000000..eecd972 --- /dev/null +++ b/database/migrations/2024_02_28_120923_create_guessing_guesses_table.php @@ -0,0 +1,35 @@ +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 index 0000000..48681c1 --- /dev/null +++ b/database/migrations/2024_02_28_141025_create_guessing_winners_table.php @@ -0,0 +1,38 @@ +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'); + } +}; -- 2.39.2