->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);
}
->withPivot('accept_comms', 'accept_tracker');
}
+ public function guesses() {
+ return $this->hasMany(GuessingGuess::class);
+ }
+
public function organization() {
return $this->belongsTo(Organization::class);
}
'chat' => 'boolean',
'chat_commands' => 'array',
'chat_settings' => 'array',
+ 'guessing_start' => 'datetime',
+ 'guessing_end' => 'datetime',
'languages' => 'array',
'join' => 'boolean',
];
--- /dev/null
+<?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);
+ }
+
+}
--- /dev/null
+<?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);
+ }
+
+}
namespace App\TwitchBot;
use App\Models\Channel;
+use Illuminate\Support\Arr;
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;
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);
--- /dev/null
+<?php
+
+namespace App\TwitchBot;
+
+class GuessingCancelCommand extends ChatCommand {
+
+ public function execute($args) {
+ if ($this->chanel->hasActiveGuessing()) {
+ $this->channel->clearGuessing();
+ $this->messageChannel('Guessing game cancelled');
+ }
+ }
+
+}
+
+?>
--- /dev/null
+<?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();
+ }
+
+}
+
+?>
--- /dev/null
+<?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');
+ }
+
+}
+
+?>
--- /dev/null
+<?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');
+ }
+
+}
+
+?>
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';
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) {
$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());
}
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');
+ }
};
--- /dev/null
+<?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');
+ });
+ }
+};
--- /dev/null
+<?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');
+ }
+};
--- /dev/null
+<?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');
+ }
+};