mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Mechanical `gofmt -w ./internal ./cmd`. Mostly struct-field realignment that had drifted, plus a few trailing-newline fixes. No behaviour change — gofmt is semantics-preserving, and build/vet/test are green either side. Split out from the code-review fixes that follow so those stay reviewable instead of hiding inside a wall of realignment.
179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"strings"
|
|
|
|
"maunium.net/go/mautrix"
|
|
"maunium.net/go/mautrix/id"
|
|
|
|
"gogobee/internal/db"
|
|
)
|
|
|
|
// FlipPlugin handles !flip (coin flip) restricted to the games room.
|
|
type FlipPlugin struct {
|
|
Base
|
|
}
|
|
|
|
func NewFlipPlugin(client *mautrix.Client) *FlipPlugin {
|
|
return &FlipPlugin{Base: NewBase(client)}
|
|
}
|
|
|
|
func (p *FlipPlugin) Name() string { return "flip" }
|
|
|
|
func (p *FlipPlugin) Commands() []CommandDef {
|
|
return []CommandDef{
|
|
{Name: "flip", Description: "Flip a coin", Usage: "!flip", Category: "Games"},
|
|
{Name: "games", Description: "List available games", Usage: "!games", Category: "Games"},
|
|
{Name: "twinbeeboard", Description: "GogoBee's victory record", Usage: "!twinbeeboard", Category: "Games"},
|
|
}
|
|
}
|
|
|
|
func (p *FlipPlugin) Init() error { return nil }
|
|
func (p *FlipPlugin) OnReaction(_ ReactionContext) error { return nil }
|
|
|
|
func (p *FlipPlugin) OnMessage(ctx MessageContext) error {
|
|
switch {
|
|
case p.IsCommand(ctx.Body, "flip"):
|
|
if !isGamesRoom(ctx.RoomID) {
|
|
gr := gamesRoom()
|
|
if gr != "" {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Games are only available in the games channel!")
|
|
}
|
|
}
|
|
return p.handleFlip(ctx)
|
|
case p.IsCommand(ctx.Body, "games"):
|
|
return p.handleGames(ctx)
|
|
case p.IsCommand(ctx.Body, "twinbeeboard"):
|
|
return p.handleLoseboard(ctx)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *FlipPlugin) handleFlip(ctx MessageContext) error {
|
|
if rand.IntN(2) == 0 {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, "🪙 **Heads!**")
|
|
}
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, "🪙 **Tails!**")
|
|
}
|
|
|
|
func (p *FlipPlugin) handleGames(ctx MessageContext) error {
|
|
gr := gamesRoom()
|
|
roomNote := ""
|
|
if gr != "" {
|
|
roomNote = "\n\nAll games are played in the games channel."
|
|
}
|
|
|
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
|
"🎮 **Available Games**\n\n"+
|
|
"**!flip** — Coin flip\n"+
|
|
"**!hangman start** — Collaborative Hangman\n"+
|
|
"**!blackjack €amount** — Blackjack (1-4 players vs dealer)\n"+
|
|
"**!uno €amount** — UNO (solo or multiplayer, classic or No Mercy)\n"+
|
|
"**!holdem join** — Texas Hold'em poker (2-9 players)\n"+
|
|
"**!wordle <word>** — Daily cooperative Wordle\n"+
|
|
"**!trivia** — Trivia questions\n\n"+
|
|
"**Economy:**\n"+
|
|
"**!balance** — Check your euros\n"+
|
|
"**!baltop** — Euro leaderboard\n"+
|
|
"**!baltransfer @user €amount** — Send euros\n"+
|
|
"**!hangboard** — Hangman leaderboard\n"+
|
|
"**!bjboard** — Blackjack leaderboard\n"+
|
|
"**!twinbeeboard** — Bot defeat leaderboard"+
|
|
roomNote)
|
|
}
|
|
|
|
func (p *FlipPlugin) handleLoseboard(ctx MessageContext) error {
|
|
d := db.Get()
|
|
rows, err := d.Query(
|
|
`SELECT user_id, game, losses FROM bot_defeats
|
|
WHERE losses > 0
|
|
ORDER BY losses DESC LIMIT 15`)
|
|
if err != nil {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load twinbeeboard.")
|
|
}
|
|
defer rows.Close()
|
|
|
|
// Aggregate total losses per user and track per-game breakdown
|
|
type userStats struct {
|
|
total int
|
|
breakdown map[string]int
|
|
}
|
|
users := make(map[string]*userStats)
|
|
var order []string
|
|
|
|
for rows.Next() {
|
|
var uid, game string
|
|
var losses int
|
|
if err := rows.Scan(&uid, &game, &losses); err != nil {
|
|
continue
|
|
}
|
|
if _, ok := users[uid]; !ok {
|
|
users[uid] = &userStats{breakdown: make(map[string]int)}
|
|
order = append(order, uid)
|
|
}
|
|
users[uid].total += losses
|
|
users[uid].breakdown[game] = losses
|
|
}
|
|
|
|
if len(order) == 0 {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, "🐝 No victories yet. GogoBee is patient.")
|
|
}
|
|
|
|
// Sort by total losses descending
|
|
for i := 0; i < len(order); i++ {
|
|
for j := i + 1; j < len(order); j++ {
|
|
if users[order[j]].total > users[order[i]].total {
|
|
order[i], order[j] = order[j], order[i]
|
|
}
|
|
}
|
|
}
|
|
if len(order) > 10 {
|
|
order = order[:10]
|
|
}
|
|
|
|
gameLabels := map[string]string{
|
|
"blackjack": "BJ",
|
|
"uno": "UNO",
|
|
"uno_multi": "UNO MP",
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("🐝 **GogoBee's Trophy Wall**\n\n")
|
|
for i, uid := range order {
|
|
name := p.DisplayName(id.UserID(uid))
|
|
stats := users[uid]
|
|
var parts []string
|
|
for game, count := range stats.breakdown {
|
|
label := gameLabels[game]
|
|
if label == "" {
|
|
label = game
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%s: %d", label, count))
|
|
}
|
|
sb.WriteString(fmt.Sprintf("%d. **%s** — %d losses (%s)\n",
|
|
i+1, name, stats.total, strings.Join(parts, ", ")))
|
|
}
|
|
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
|
}
|
|
|
|
// recordBotDefeat increments the bot defeat counter for a user in a specific game.
|
|
// This is a package-level function so all game plugins can call it.
|
|
func recordBotDefeat(userID id.UserID, game string) {
|
|
db.Exec("flip: record bot defeat",
|
|
`INSERT INTO bot_defeats (user_id, game, losses)
|
|
VALUES (?, ?, 1)
|
|
ON CONFLICT(user_id, game) DO UPDATE SET
|
|
losses = losses + 1`,
|
|
string(userID), game,
|
|
)
|
|
}
|
|
|
|
// redirectToGamesRoom returns the room ID for games-restricted redirect.
|
|
func redirectToGamesRoom(sender id.UserID) string {
|
|
_ = sender
|
|
return "Games are only available in the games channel!"
|
|
}
|