From b0be1f99c8c377970e8e56bcb86b094e5bf55fac Mon Sep 17 00:00:00 2001 From: bryce Date: Tue, 15 Jul 2025 20:24:16 +1200 Subject: [PATCH] Added !poker and auth entry point --- cmd/bot/main.go | 17 ++++- cmd/oauth/main.go | 13 ++++ internal/auth/oauth.go | 2 +- internal/chat/handler.go | 6 ++ internal/chat/poker.go | 154 ++++++++++++++++++++++++++++++++++++++ internal/points/points.go | 84 +++++++++++++++++++++ 6 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 cmd/oauth/main.go create mode 100644 internal/chat/poker.go create mode 100644 internal/points/points.go diff --git a/cmd/bot/main.go b/cmd/bot/main.go index f0fabdc..c9a3a66 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -4,9 +4,10 @@ import ( "log" "os" - "streambot_twitch/internal/auth" + auth "streambot_twitch/internal/auth" "streambot_twitch/internal/chat" "streambot_twitch/internal/commands" + "streambot_twitch/internal/points" twitchapi "streambot_twitch/internal/twitchapi" tests "streambot_twitch/internal/tests" twitch "github.com/gempir/go-twitch-irc/v4" @@ -14,7 +15,7 @@ import ( func main() { go func() { - if err := oauth.StartOAuthServer(":8080"); err != nil { + if err := auth.StartOAuthServer(":8080"); err != nil { log.Fatalf("OAuth server error: %v", err) } }() @@ -22,9 +23,9 @@ func main() { log.Println("OAuth server started on port 8080") botUsername := os.Getenv("nz_chatterbot") - botOAuth :+ os.Getenv("BOT_OAUTH") + botOAuth := os.Getenv("BOT_OAUTH") channel := os Getenv("brycefromnz101") - clientID := os.Getenv("TWITCH_CLIENTID") + clientID := os.Getenv("TWITCH_CLIENT_ID") clientSecret := os.Getenv("TWITCH_CLIENT_SECRET") botChannel := botUsername @@ -52,6 +53,14 @@ log.Println("OAuth server started on port 8080") log.Println("All startup tests passed. Starting bot. . .") // END of TESTS + // load chat points + if err := points.LoadPoints(); err != nil { + log.Fatalf("Failed to load points: %v", err) + } + + // make sure points are saved on exit + defer points.SavePoints() + // load custom commands if err := commands.LoadCommands(); err != nil { log.Fatalf("Failed to Load Custom Commands: %v", err) diff --git a/cmd/oauth/main.go b/cmd/oauth/main.go new file mode 100644 index 0000000..2ca35bc --- /dev/null +++ b/cmd/oauth/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + auth "streambot_twitch/internal/auth" +) + +func main() { + if err := auth.StartOAuthServer(":8080"); err != nil { + log.Fatalf("OAuth server error: %v", err) + } +} diff --git a/internal/auth/oauth.go b/internal/auth/oauth.go index 5f82d42..91151d3 100644 --- a/internal/auth/oauth.go +++ b/internal/auth/oauth.go @@ -1,4 +1,4 @@ -package oauth +package auth import ( "encoding/json" diff --git a/internal/chat/handler.go b/internal/chat/handler.go index 4dbdedc..a36afda 100644 --- a/internal/chat/handler.go +++ b/internal/chat/handler.go @@ -91,6 +91,12 @@ func HandleMessage(client *twitch.Client, apiClient interface{}, message twitch. return } + // dispatch poker/table commands + if strings.HasPrefix(strings.ToLower(msg), "!poker") || strings.HasPrefix(strings.ToLower(msg), "!table") { + HandlePokerCommand(client, message, user, channel, msg) + return + } + // Dispatch custom/default commands HandleCustomCommands(client, message, user, channel, botUsername) } diff --git a/internal/chat/poker.go b/internal/chat/poker.go new file mode 100644 index 0000000..4fb8f3d --- /dev/null +++ b/internal/chat/poker.go @@ -0,0 +1,154 @@ +package chat + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + "time" + + twitch "github.com/gempir/go-twitch-irc/v4" + "streambot_twitch/internal/points" +) + +// card +type Card struct { + Rank, Suit string +} + +var ( + suits = []string{" of Hearts", " of Diamnods", " of Clubs", " of Spades"} + ranks = []string{"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"} + baseDeck []Card +) + +func init() { + // build 2 decks (no Jokers) 104 cards + for d := 0; d < 2; d++ { + for _, s := range suits { + for _, r := range ranks { + baseDeck = append(baseDeck, Card{Rank: r, Suit: s}) + } + } + } + rand.Seed(time.Now().UnixNano()) +} + +// impliment !poker or !table +func HandlePokerCommand( + client *twitch.Client, + message twitch.PrivateMessage, + user twitch.User, + channel, msg string, +) { + parts := strings.Fields(msg) + if len(parts) < 2 { + client.Say(channel, fmt.Sprintf("@%s Usage: !poker ", user.DisplayName)) + return + } + bet, err := strconv.Atoi(parts[1]) + if err != nil || bet <= 0 { + client.Say(channel, fmt.Sprintf("@%s Invalid bet amount", user.DisplayName)) + return + } + + login := strings.ToLower(user.Name) + // load or init user points + balance := points.GetPoints(login) + if bet > ballance { + client.Say(channel, fmt.Sprintf("@%s You have %d points, bet lower", user.DisplayName, balance)) + return + } + // Deduct the bet + balance, err = points.AddPoints(login, -bet) + if err != nil { + client.Say(channel, fmt.Sprintf("@%s Error updating points: %v", user.DisplayName, err)) + return + } + + // shuffle deck + deck := make([]Card, len(baseDeck)) + copy(deck, baseDeck) + rand.Shuffle(len(deck), func(i, j int) {deck[i], deck[j] = deck[j], deck[i]}) + + // deal 2 cards each + player := []Card{deck[0], deck[1]} + dealer := []Card{deck[2], deck[3]} + + // compute hand values + pv := handValue(player) + dv := handValue(dealer) + + // Determine outcome and payout multiplier + var net int + var outcome string + switch { + case pv > 21: + outcome = "FBPenalty Bust! You lose" + net = 0 + case pv == 21: + outcome = "BLACKJACK! GoatEmotey" + net = bet * 2 + case dv > 21: + outcome = "GoldPLZ Dealer bust! You win" + net = int(float64(bet) * 1.5) + case pv > dv: + outcome = "OhMyDog You Win" + net int(float64(bet) * 1.3) + case pv < dv: + outcome = "cmonBruh You lose" + net = 0 + default: + outcome = "PunchTrees Push" + net = bet + } + + // Add winnings + balance, err = points.AddPoints(login, net) + if err != nil { + client.Say(channel, fmt.Sprintf("@%s Error updating Points: %v", user.DisplayName, err)) + return + } + + // format a hand for display + fmtHand := func(h []Card) string { + var parts []string + for _, c := range h { + parts = append(parts, c.Rank+c.Suit) + } + return strings.Join(parts, ",") + } + + // post result + client.Say(channel, fmt.Sprintf("@%s You: [%s]=%d Dealer: [%s]=%d => %s | Bet: %d Net: %d Ballance: %d", user.DisplayName, + fmtHand(player), pv, + fmtHand(dealer), dv, + outcome, + bet, net, balance, + )) +} + +// compute BlackJack style value (A = 11 or 1) +func handValue(hand []Card) int { + total := 0 + aces := 0 + for _, c := range hand { + switch c.Rank { + case "J", "Q", "K": + total += 10 + case "A": + total += 11 + aces++ + default: + v, _ := strconv.Atoi(c.Rank) + total += v + } + } + + // convert A from 11 to 1 while busting + for total > 21 && aces > 0 { + total -= 10 + aces-- + } + return total +} diff --git a/internal/points/points.go b/internal/points/points.go new file mode 100644 index 0000000..5c32b1f --- /dev/null +++ b/internal/points/points.go @@ -0,0 +1,84 @@ +package points + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" +) + +const ( + dataDir = "internal/storage" + pointsFile = "points.json" +) + +var ( + mu sync.Mutex + pts map[string]int +) + +const DefaultPoints = 100000 + +// init or load points from disk +func LoadPoints() error { + mu.Lock() + defer mu.Unlock() + pts = make(map[string]int) + path := filepath.Join(dataDir, pointsFile) + + data, err := ioutil.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + return json.Unmarshal(data, &pts) +} + +// save balances to disk +func SavePoints() error { + mu.Lock() + data, err := json.MarshalIndent(pts, "", " ") + mu.Unlock() + if err != nil { + return err + } + if err := os.MkdirAll(dataDir, 0755); err != nil { + return err + } + return ioutil.WriteFile(filepath.Join(dataDir, pointsFile), data, 0644) +} + +// return point balance for user, init if needed +func GetPoints(user string) int { + mu.Lock() + defer mu.Unlock() + key := strings.ToLower(user) + bal, ok := pts[key] + if !ok { + pts[key] = DefaultPoints + bal = DefaultPoints + } + return bal +} + +// add points to user's bal, save & return new bal +func AddPoints(user string, delta int) (int error) { + mu.Lock() + key := strings.ToLower(user) + bal, ok := pts[key] + if !ok { + bal = DefaultPoints + } + bal += delta + pts[key] = bal + mu.Unlock() + + if err := SavePoints(); err != nil { + return bal, err + } + return bal, nil +}