- 1# === grok.tcl — AI for Eggdrop ===
- 2# - Channel control (join/leave)
- 3# botnick join #chan
- 4# botnick leave #chan
- 5# - Poker Commands:
- 6# Grok poker start [chips] ;# start/host a game (default 1000 chips)
- 7# Grok poker join ;# join host's game (2 players total)
- 8# Grok poker deal ;# deal a new hand when 2 players are seated
- 9# Grok poker hand ;# DM your hole cards again
- 10# Grok poker check|call|bet N|raise N|allin|fold
- 11# Grok poker status ;# table state (no hole cards)
- 12# Grok poker stop ;# stop and clear the table
- 13# Grok poker debug ;# debug state (for troubleshooting)
- 14
- 15package require http
- 16package require json
- 17
- 18# =======================
- 19# Configuration
- 20# =======================
- 21set ai(link) "http://127.0.0.1:5000/chat" ;# Flask API endpoint for Grok
- 22set ai(secured) "0" ;# HTTPS disabled (0 = HTTP, 1 = HTTPS)
- 23set ai(rate_limit) 5 ;# Seconds between user requests per channel (non-poker commands)
- 24set ai(max_msg_length) 400 ;# Max IRC message length
- 25set ai(max_request_age) 3600 ;# Clean request records older than 1 hour
- 26array set ai_last_request {} ;# Track last request time per user-channel
- 27array set poker_games {} ;# Track poker games per channel
- 28
- 29# Poker defaults
- 30set poker(default_chips) 1000
- 31set poker(sb) 5
- 32set poker(bb) 10
- 33
- 34# Channel control whitelist (case-insensitive nicks) "Eck TeLe"
- 35set ai(channel_admins) {Eck TeLe}
- 36
- 37# Enable HTTPS if configured
- 38if {$ai(secured) == 1} {
- 39 package require tls
- 40 http::register https 443 [list ::tls::socket -autoservername true]
- 41}
- 42
- 43# Bind to public messages
- 44bind pubm - * ai:botnickTalk
- 45setudef flag grok
- 46
- 47# =======================
- 48# Core addressing & chat
- 49# =======================
- 50proc ai:botnickTalk {nick uhost handle target args} {
- 51 global botnick ai ai_last_request
- 52 set message [join [lrange $args 0 end] " "]
- 53 if {$message eq ""} { return }
- 54
- 55 # Addressing checks for "$botnick ..." or "grok ..."
- 56 if {![string match -nocase "$botnick*" $message] && ![string match -nocase "grok*" $message]} {
- 57 return
- 58 }
- 59 if {![channel get $target grok]} {
- 60 putquick "PRIVMSG $target :Grok is not enabled for this channel."
- 61 return
- 62 }
- 63
- 64 # Strip addressing
- 65 set msg $message
- 66 foreach prefix [list $botnick "grok"] {
- 67 foreach sep {" " ":" ","} {
- 68 if {[string match -nocase "${prefix}${sep}*" $msg]} {
- 69 set msg [string trim [string range $msg [expr {[string length $prefix]+1}] end]]
- 70 }
- 71 }
- 72 if {[string equal -nocase $prefix $msg]} { set msg "" }
- 73 }
- 74
- 75 # Built-in utility commands
- 76 if {[string equal -nocase $msg "channels"]} { ai:list_channels $nick $target; return }
- 77 if {[string equal -nocase $msg "reload"]} { ai:reload_scripts $nick $target $uhost; return }
- 78 if {[string equal -nocase $msg "debug"]} { ai:debug_channel $nick $target; return }
- 79
- 80 # ---- Channel control (restricted) ----
- 81 # botnick join #chan
- 82 # botnick leave #chan (or "part")
- 83 if {[regexp -nocase {^(join|leave|part)\s+(\S+)} $msg -> subcmd chan]} {
- 84 ai:handle_channel_control $nick $target $subcmd $chan
- 85 return
- 86 }
- 87
- 88 # Poker router (kept under "Grok poker ...")
- 89 if {[string match -nocase "poker *" $msg]} {
- 90 poker:handle_command $nick $target [string range $msg 6 end]
- 91 return
- 92 }
- 93
- 94 # Rate limit for general chat
- 95 set key "$nick:$target"
- 96 set now [clock seconds]
- 97 if {[info exists ai_last_request($key)] && ($now - $ai_last_request($key)) < $ai(rate_limit)} {
- 98 putquick "PRIVMSG $target :Please wait a few seconds before asking again!"
- 99 return
- 100 }
- 101 set ai_last_request($key) $now
- 102 ai:clean_old_requests $now
- 103
- 104 set encoded [::http::formatQuery message $msg nick $nick]
- 105 set url "$ai(link)?$encoded"
- 106 ::http::geturl $url -timeout 30000 -command [list ai:callback $target $nick] -headers {Accept application/json}
- 107}
- 108
- 109# ---- Channel control helpers ----
- 110proc ai:is_channel_admin {nick} {
- 111 global ai
- 112 foreach a $ai(channel_admins) {
- 113 if {[string equal -nocase $a $nick]} { return 1 }
- 114 }
- 115 return 0
- 116}
- 117proc ai:valid_channel_name {c} {
- 118 # basic sanity: must start with # and contain typical safe chars
- 119 return [expr {[regexp {^#[A-Za-z0-9_\-\[\]\\^{}|`]+$} $c] ? 1 : 0}]
- 120}
- 121proc ai:handle_channel_control {nick target subcmd rawchan} {
- 122 if {![ai:is_channel_admin $nick]} {
- 123 putquick "PRIVMSG $target :Sorry $nick, only Eck or Jordan can do that."
- 124 return
- 125 }
- 126 # if they typed "leave" with no #channel, default to current target (if it's a channel)
- 127 set chan $rawchan
- 128 if {$subcmd ni {join leave part}} { return }
- 129 if {$subcmd in {leave part} && ($chan eq "" || ![string match "#*" $chan]) && [string match "#*" $target]} {
- 130 set chan $target
- 131 }
- 132 if {![string match "#*" $chan]} {
- 133 putquick "PRIVMSG $target :Please specify a channel, e.g. #example"
- 134 return
- 135 }
- 136 if {![ai:valid_channel_name $chan]} {
- 137 putquick "PRIVMSG $target :That channel name looks invalid."
- 138 return
- 139 }
- 140
- 141 if {$subcmd eq "join"} {
- 142 ai:join_channel $nick $target $chan
- 143 } else {
- 144 ai:leave_channel $nick $target $chan
- 145 }
- 146}
- 147proc ai:join_channel {nick where chan} {
- 148 # Add to Eggdrop's channel list if needed, then JOIN
- 149 if {![validchan $chan]} { channel add $chan }
- 150 if {[lsearch -nocase [channels] $chan] >= 0} {
- 151 putquick "PRIVMSG $where :I'm already in $chan."
- 152 return
- 153 }
- 154 putquick "JOIN $chan"
- 155 putquick "PRIVMSG $where :Joining $chan (requested by $nick)."
- 156}
- 157proc ai:leave_channel {nick where chan} {
- 158 if {[lsearch -nocase [channels] $chan] < 0} {
- 159 putquick "PRIVMSG $where :I'm not in $chan."
- 160 return
- 161 }
- 162 putquick "PART $chan :Requested by $nick"
- 163 # Remove from channel list so the bot won't auto-rejoin
- 164 if {[validchan $chan]} { channel remove $chan }
- 165 putquick "PRIVMSG $where :Left $chan."
- 166}
- 167
- 168# ---- Utilities ----
- 169proc ai:debug_channel {nick target} {
- 170 global botnick
- 171 set chans [channels]
- 172 set users [chanlist $target]
- 173 set modes [getchanmode $target]
- 174 set in [expr {[lsearch -nocase $chans $target] >= 0}]
- 175 putquick "PRIVMSG $target :Debug: in=$in, users=[join $users , ], modes=$modes, Grok=[channel get $target grok]"
- 176}
- 177proc ai:list_channels {nick target} {
- 178 set chans [channels]
- 179 if {![llength $chans]} { putquick "PRIVMSG $target :I'm not in any channels."; return }
- 180 putquick "PRIVMSG $target :I'm currently in: [join $chans ", "]"
- 181}
- 182proc ai:reload_scripts {nick target uhost} {
- 183 if {![matchattr [nick2hand $nick $uhost] n]} {
- 184 putquick "PRIVMSG $target :$nick, you don't have permission to reload scripts."
- 185 return
- 186 }
- 187 if {[catch {rehash} err]} {
- 188 putquick "PRIVMSG $target :Failed to reload scripts: $err"
- 189 } else {
- 190 putquick "PRIVMSG $target :Scripts reloaded successfully by $nick."
- 191 }
- 192}
- 193proc ai:clean_old_requests {now} {
- 194 global ai ai_last_request
- 195 foreach key [array names ai_last_request] {
- 196 if {($now - $ai_last_request($key)) > $ai(max_request_age)} {
- 197 unset ai_last_request($key)
- 198 }
- 199 }
- 200}
- 201proc ai:callback {target nick token} {
- 202 set status [::http::status $token]
- 203 set code [::http::ncode $token]
- 204 set body [::http::data $token]
- 205 ::http::cleanup $token
- 206
- 207 if {$status ne "ok" || $code != 200} {
- 208 putquick "PRIVMSG $target :Sorry, I couldn't connect to Grok! (Status: $status, Code: $code)"
- 209 return
- 210 }
- 211 if {[catch {set d [::json::json2dict $body]} err]} {
- 212 putquick "PRIVMSG $target :Sorry, I couldn't parse the Grok response!"
- 213 return
- 214 }
- 215 set out [dict get $d reply]
- 216 if {$out eq ""} { set out "Sorry, no response from Grok!" }
- 217 ai:displayMessage $out $target $nick
- 218}
- 219proc ai:displayMessage {output target nick} {
- 220 global ai
- 221 set output [string map {\\\" "\"" \\n "\n" \\t "\t"} $output]
- 222 foreach line [split $output "\n"] {
- 223 if {$line eq ""} { continue }
- 224 while {[string length $line] > $ai(max_msg_length)} {
- 225 set chunk [string range $line 0 [expr {$ai(max_msg_length)-1}]]
- 226 set lastSpace [string last " " $chunk]
- 227 if {$lastSpace > 0} {
- 228 set chunk [string range $line 0 [expr {$lastSpace-1}]]
- 229 set line [string range $line $lastSpace end]
- 230 } else {
- 231 set line [string range $line $ai(max_msg_length) end]
- 232 }
- 233 putquick "PRIVMSG $target :$chunk"
- 234 }
- 235 putquick "PRIVMSG $target :$line"
- 236 }
- 237}
- 238
- 239# =======================
- 240# Poker game
- 241# =======================
- 242
- 243# Public command router
- 244proc poker:handle_command {nick target raw} {
- 245 set cmd [string tolower [string trim $raw]]
- 246
- 247 if {$cmd eq "start" || [string match "start *" $cmd]} {
- 248 set chips [lindex $cmd 1]
- 249 if {![string is integer -strict $chips] || $chips <= 0} { set chips "" }
- 250 poker:start_game $nick $target $chips
- 251 return
- 252 }
- 253 if {$cmd eq "join"} { poker:join_game $nick $target; return }
- 254 if {$cmd eq "deal"} { poker:deal_hand $nick $target; return }
- 255 if {$cmd eq "hand"} { poker:send_hole_cards $nick $target; return }
- 256 if {$cmd eq "status"} { poker:status $nick $target; return }
- 257 if {$cmd eq "stop"} { poker:stop_game $nick $target; return }
- 258 if {$cmd eq "debug"} { poker:debug_state $nick $target; return }
- 259
- 260 # Actions
- 261 if {$cmd eq "check"} { poker:action_check $nick $target; return }
- 262 if {$cmd eq "call"} { poker:action_call $nick $target; return }
- 263 if {$cmd eq "fold"} { poker:action_fold $nick $target; return }
- 264 if {[string match "bet *" $cmd]} {
- 265 set amt [lindex $cmd 1]
- 266 poker:action_bet $nick $target $amt
- 267 return
- 268 }
- 269 if {[string match "raise *" $cmd]} {
- 270 set amt [lindex $cmd 1]
- 271 poker:action_raise $nick $target $amt
- 272 return
- 273 }
- 274 if {$cmd eq "allin"} {
- 275 poker:action_allin $nick $target
- 276 return
- 277 }
- 278
- 279 putquick "PRIVMSG $target :Unknown poker command. Try: start [chips], join, deal, hand, status, stop, or actions: check, call, bet <N>, raise <N>, allin, fold"
- 280}
- 281
- 282# ---- Table lifecycle ----
- 283
- 284proc poker:start_game {host target chips} {
- 285 global poker_games poker
- 286 if {[info exists poker_games($target)]} {
- 287 putquick "PRIVMSG $target :A poker table is already running. Use 'Grok poker join' to sit."
- 288 return
- 289 }
- 290 if {$chips eq ""} { set chips $poker(default_chips) }
- 291 set game [dict create \
- 292 host $host \
- 293 players [list $host] \
- 294 stacks [dict create $host $chips] \
- 295 seated 1 \
- 296 stage "lobby" \
- 297 dealer_idx 0 \
- 298 deck {} \
- 299 hands [dict create] \
- 300 community {} \
- 301 pot 0 \
- 302 round_bet [dict create] \
- 303 current_bet 0 \
- 304 to_act_idx -1 \
- 305 last_action "" \
- 306 sb $poker(sb) \
- 307 bb $poker(bb) \
- 308 ]
- 309 dict set game round_bet $host 0
- 310 set poker_games($target) $game
- 311 putquick "PRIVMSG $target :Poker table opened by $host — stacks=$chips chips. Another player can 'Grok poker join'."
- 312}
- 313
- 314proc poker:join_game {nick target} {
- 315 global poker_games poker
- 316 if {![info exists poker_games($target)]} {
- 317 putquick "PRIVMSG $target :No poker table yet. Start with 'Grok poker start [chips]'."
- 318 return
- 319 }
- 320 set game $poker_games($target)
- 321
- 322 if {[lsearch -exact [dict get $game players] $nick] >= 0} {
- 323 putquick "PRIVMSG $target :$nick, you're already seated."
- 324 return
- 325 }
- 326 if {[llength [dict get $game players]] >= 2} {
- 327 putquick "PRIVMSG $target :Table is full (heads-up)."
- 328 return
- 329 }
- 330
- 331 # Sit second player with same stack as host unless specified later
- 332 set host [dict get $game host]
- 333 set host_stack [dict get $game stacks $host]
- 334 dict lappend game players $nick
- 335 dict set game stacks $nick $host_stack
- 336 dict set game round_bet $nick 0
- 337 dict set game seated 2
- 338 set poker_games($target) $game
- 339
- 340 putquick "PRIVMSG $target :$nick sits with $host_stack chips. Type 'Grok poker deal' to start a hand."
- 341}
- 342
- 343proc poker:deal_hand {nick target} {
- 344 global poker_games
- 345 if {![info exists poker_games($target)]} {
- 346 putquick "PRIVMSG $target :No table. 'Grok poker start' to create one."
- 347 return
- 348 }
- 349 set game $poker_games($target)
- 350 if {[dict get $game seated] < 2} {
- 351 putquick "PRIVMSG $target :Need 2 players to deal."
- 352 return
- 353 }
- 354
- 355 # Rotate dealer each hand (0/1)
- 356 set dealer [dict get $game dealer_idx]
- 357 set dealer [expr {($dealer + 1) % 2}]
- 358 dict set game dealer_idx $dealer
- 359
- 360 # Fresh deck
- 361 set deck [poker:build_deck]
- 362 # Deal 2 cards to each (private)
- 363 set players [dict get $game players]
- 364 set hands [dict create]
- 365 foreach p $players {
- 366 set c1 [poker:draw_one deck]
- 367 set c2 [poker:draw_one deck]
- 368 dict set hands $p [list $c1 $c2]
- 369 # PM hole cards
- 370 putquick "PRIVMSG $p :Your hole cards: $c1 $c2"
- 371 }
- 372
- 373 # Reset state
- 374 dict set game deck $deck
- 375 dict set game hands $hands
- 376 dict set game community {}
- 377 dict set game pot 0
- 378 dict set game round_bet [dict create]
- 379 foreach p $players { dict set game round_bet $p 0 }
- 380 dict set game current_bet 0
- 381 dict set game last_action ""
- 382 dict set game stage "preflop"
- 383
- 384 # Blinds (heads-up: dealer = SB, other = BB)
- 385 set sbp [lindex $players $dealer]
- 386 set bbp [lindex $players [expr {1 - $dealer}]]
- 387 set sb [dict get $game sb]
- 388 set bb [dict get $game bb]
- 389
- 390 if {![poker:charge_player game $sbp $sb]} {
- 391 putquick "PRIVMSG $target :$sbp is out of chips. Game stopped."
- 392 unset ::poker_games($target)
- 393 return
- 394 }
- 395 dict set game round_bet $sbp $sb
- 396
- 397 if {![poker:charge_player game $bbp $bb]} {
- 398 putquick "PRIVMSG $target :$bbp is out of chips. Game stopped."
- 399 unset ::poker_games($target)
- 400 return
- 401 }
- 402 dict set game round_bet $bbp $bb
- 403 dict incr game pot [expr {$sb + $bb}]
- 404 dict set game current_bet $bb
- 405
- 406 # Preflop action: in heads-up, dealer (SB) acts first
- 407 dict set game to_act_idx $dealer
- 408 set ::poker_games($target) $game
- 409
- 410 putquick "PRIVMSG $target :New hand! Dealer: $sbp (SB=$sb), $bbp is BB=$bb. Pot=[dict get $game pot]. $sbp to act."
- 411}
- 412
- 413proc poker:stop_game {nick target} {
- 414 global poker_games
- 415 if {![info exists poker_games($target)]} {
- 416 putquick "PRIVMSG $target :No poker game running."
- 417 return
- 418 }
- 419 unset poker_games($target)
- 420 putquick "PRIVMSG $target :Poker table closed."
- 421}
- 422
- 423# ---- Actions ----
- 424
- 425proc poker:actor {game} {
- 426 set idx [dict get $game to_act_idx]
- 427 if {$idx < 0} { return "" }
- 428 return [lindex [dict get $game players] $idx]
- 429}
- 430
- 431proc poker:ensure_turn {nick target} {
- 432 global poker_games
- 433 set game $poker_games($target)
- 434 return [expr {[poker:actor $game] eq $nick}]
- 435}
- 436
- 437proc poker:action_check {nick target} {
- 438 global poker_games
- 439 if {![poker:ensure_turn $nick $target]} { poker:notyourturn $target; return }
- 440 set game $poker_games($target)
- 441 if {[dict get $game current_bet] > [dict get $game round_bet $nick]} {
- 442 putquick "PRIVMSG $target :You cannot check; there is a bet to call."
- 443 return
- 444 }
- 445 dict set game last_action "check"
- 446 dict set game to_act_idx [poker:other_idx $game]
- 447 set ::poker_games($target) $game
- 448 putquick "PRIVMSG $target :$nick checks."
- 449 poker:maybe_round_complete $target
- 450}
- 451
- 452proc poker:action_call {nick target} {
- 453 global poker_games
- 454 if {![poker:ensure_turn $nick $target]} { poker:notyourturn $target; return }
- 455 set game $poker_games($target)
- 456 set tocall [expr {[dict get $game current_bet] - [dict get $game round_bet $nick]}]
- 457 if {$tocall <= 0} {
- 458 putquick "PRIVMSG $target :Nothing to call."
- 459 return
- 460 }
- 461 if {![poker:charge_player game $nick $tocall]} {
- 462 set rem [dict get $game stacks $nick]
- 463 if {$rem <= 0} {
- 464 putquick "PRIVMSG $target :$nick is out of chips (cannot call)."
- 465 return
- 466 }
- 467 dict incr game round_bet $nick $rem
- 468 dict incr game pot $rem
- 469 dict set game to_act_idx [poker:other_idx $game]
- 470 set ::poker_games($target) $game
- 471 putquick "PRIVMSG $target :$nick goes all-in for $rem (call short). Pot=[dict get $game pot]."
- 472 poker:maybe_round_complete $target
- 473 return
- 474 }
- 475 dict incr game round_bet $nick $tocall
- 476 dict incr game pot $tocall
- 477 dict set game to_act_idx [poker:other_idx $game]
- 478 set ::poker_games($target) $game
- 479 putquick "PRIVMSG $target :$nick calls $tocall. Pot=[dict get $game pot]."
- 480 poker:maybe_round_complete $target
- 481}
- 482
- 483proc poker:action_bet {nick target amt} {
- 484 global poker_games
- 485 if {![poker:ensure_turn $nick $target]} { poker:notyourturn $target; return }
- 486 if {![string is integer -strict $amt] || $amt <= 0} {
- 487 putquick "PRIVMSG $target :Usage: bet <amount>"
- 488 return
- 489 }
- 490 set game $poker_games($target)
- 491 if {[dict get $game current_bet] > 0} {
- 492 putquick "PRIVMSG $target :There is already a bet. Use 'raise <amount>'."
- 493 return
- 494 }
- 495 set have [dict get $game stacks $nick]
- 496 if {$amt > $have} { set amt $have } ;# all-in if exceeds
- 497 if {![poker:charge_player game $nick $amt]} {
- 498 putquick "PRIVMSG $target :You have no chips."
- 499 return
- 500 }
- 501 dict incr game round_bet $nick $amt
- 502 dict incr game pot $amt
- 503 dict set game current_bet [dict get $game round_bet $nick]
- 504 dict set game last_action "bet"
- 505 dict set game to_act_idx [poker:other_idx $game]
- 506 set ::poker_games($target) $game
- 507 putquick "PRIVMSG $target :$nick bets $amt. Pot=[dict get $game pot]."
- 508}
- 509
- 510proc poker:action_raise {nick target amt} {
- 511 global poker_games
- 512 if {![poker:ensure_turn $nick $target]} { poker:notyourturn $target; return }
- 513 if {![string is integer -strict $amt] || $amt <= 0} {
- 514 putquick "PRIVMSG $target :Usage: raise <amount> (amount = raise TO, not raise BY)"
- 515 return
- 516 }
- 517 set game $poker_games($target)
- 518 set cur [dict get $game current_bet]
- 519 if {$cur <= 0} {
- 520 putquick "PRIVMSG $target :No bet to raise. Use 'bet <amount>' instead."
- 521 return
- 522 }
- 523 set already [dict get $game round_bet $nick]
- 524 set need [expr {$amt - $already}]
- 525 if {$need <= 0} {
- 526 putquick "PRIVMSG $target :You already have $already in this round."
- 527 return
- 528 }
- 529 set have [dict get $game stacks $nick]
- 530 if {$need > $have} { set need $have } ;# all-in raise cap
- 531
- 532 if {![poker:charge_player game $nick $need]} {
- 533 putquick "PRIVMSG $target :You have no chips."
- 534 return
- 535 }
- 536 dict incr game round_bet $nick $need
- 537 dict incr game pot $need
- 538 dict set game current_bet [dict get $game round_bet $nick]
- 539 dict set game last_action "raise"
- 540 dict set game to_act_idx [poker:other_idx $game]
- 541 set ::poker_games($target) $game
- 542 putquick "PRIVMSG $target :$nick raises to [dict get $game current_bet]. Pot=[dict get $game pot]."
- 543}
- 544
- 545proc poker:action_allin {nick target} {
- 546 global poker_games
- 547 if {![poker:ensure_turn $nick $target]} { poker:notyourturn $target; return }
- 548 set game $poker_games($target)
- 549 set stack [dict get $game stacks $nick]
- 550 if {$stack <= 0} {
- 551 putquick "PRIVMSG $target :$nick has no chips."
- 552 return
- 553 }
- 554 dict incr game round_bet $nick $stack
- 555 dict incr game pot $stack
- 556 dict set game stacks $nick 0
- 557 if {[dict get $game round_bet $nick] > [dict get $game current_bet]} {
- 558 dict set game current_bet [dict get $game round_bet $nick]
- 559 }
- 560 dict set game to_act_idx [poker:other_idx $game]
- 561 set ::poker_games($target) $game
- 562 putquick "PRIVMSG $target :$nick is ALL-IN with $stack! Pot=[dict get $game pot]."
- 563 poker:maybe_round_complete $target
- 564}
- 565
- 566proc poker:action_fold {nick target} {
- 567 global poker_games
- 568 if {![poker:ensure_turn $nick $target]} { poker:notyourturn $target; return }
- 569 set game $poker_games($target)
- 570 set opp [poker:other_player $game]
- 571 set pot [dict get $game pot]
- 572 dict incr game stacks $opp $pot
- 573 putquick "PRIVMSG $target :$nick folds. $opp wins $pot chips."
- 574 dict set game pot 0
- 575 dict set game stage "lobby"
- 576 dict set game current_bet 0
- 577 dict set game to_act_idx -1
- 578 dict set game community {}
- 579 dict set game hands [dict create]
- 580 set ::poker_games($target) $game
- 581}
- 582
- 583proc poker:notyourturn {target} {
- 584 global poker_games
- 585 set game $poker_games($target)
- 586 putquick "PRIVMSG $target :It's [poker:actor $game]'s turn."
- 587}
- 588
- 589# ---- Round & stage progression ----
- 590
- 591proc poker:maybe_round_complete {target} {
- 592 global poker_games
- 593 if {![info exists poker_games($target)]} { return }
- 594 set game $poker_games($target)
- 595
- 596 set p1 [lindex [dict get $game players] 0]
- 597 set p2 [lindex [dict get $game players] 1]
- 598 set b1 [dict get $game round_bet $p1]
- 599 set b2 [dict get $game round_bet $p2]
- 600 set cur [dict get $game current_bet]
- 601
- 602 if {$cur == 0} {
- 603 if {[dict get $game last_action] eq "check"} {
- 604 poker:advance_stage $target
- 605 return
- 606 }
- 607 } else {
- 608 if {[dict get $game last_action] in {"bet" "raise"}} {
- 609 return
- 610 } else {
- 611 if {$b1 == $cur && $b2 == $cur} {
- 612 poker:advance_stage $target
- 613 return
- 614 }
- 615 }
- 616 }
- 617
- 618 putquick "PRIVMSG $target :[poker:actor $game] to act. (To call: [expr {$cur - [dict get $game round_bet [poker:actor $game]]}] )"
- 619}
- 620
- 621proc poker:advance_stage {target} {
- 622 global poker_games
- 623 set game $poker_games($target)
- 624 set stage [dict get $game stage]
- 625
- 626 dict set game round_bet [dict create]
- 627 foreach p [dict get $game players] { dict set game round_bet $p 0 }
- 628 dict set game current_bet 0
- 629 dict set game last_action ""
- 630
- 631 set dealer [dict get $game dealer_idx]
- 632 set first_post [expr {1 - $dealer}]
- 633
- 634 if {$stage eq "preflop"} {
- 635 set c1 [poker:draw_one [dict get $game deck]]
- 636 set c2 [poker:draw_one [dict get $game deck]]
- 637 set c3 [poker:draw_one [dict get $game deck]]
- 638 dict set game community [list $c1 $c2 $c3]
- 639 dict set game stage "flop"
- 640 dict set game to_act_idx $first_post
- 641 set ::poker_games($target) $game
- 642 putquick "PRIVMSG $target :Flop: [join [dict get $game community] { }]. [poker:actor $game] to act."
- 643 return
- 644 }
- 645
- 646 if {$stage eq "flop"} {
- 647 set c [poker:draw_one [dict get $game deck]]
- 648 dict set game community [concat [dict get $game community] [list $c]]
- 649 dict set game stage "turn"
- 650 dict set game to_act_idx $first_post
- 651 set ::poker_games($target) $game
- 652 putquick "PRIVMSG $target :Turn: [join [dict get $game community] { }]. [poker:actor $game] to act."
- 653 return
- 654 }
- 655
- 656 if {$stage eq "turn"} {
- 657 set c [poker:draw_one [dict get $game deck]]
- 658 dict set game community [concat [dict get $game community] [list $c]]
- 659 dict set game stage "river"
- 660 dict set game to_act_idx $first_post
- 661 set ::poker_games($target) $game
- 662 putquick "PRIVMSG $target :River: [join [dict get $game community] { }]. [poker:actor $game] to act."
- 663 return
- 664 }
- 665
- 666 if {$stage eq "river"} {
- 667 dict set game stage "showdown"
- 668 set ::poker_games($target) $game
- 669 poker:showdown $target
- 670 return
- 671 }
- 672}
- 673
- 674# ---- Info & helpers ----
- 675proc poker:status {nick target} {
- 676 global poker_games
- 677 if {![info exists poker_games($target)]} {
- 678 putquick "PRIVMSG $target :No poker table running."
- 679 return
- 680 }
- 681 set g $poker_games($target)
- 682 set players [dict get $g players]
- 683 set stacks [list]
- 684 foreach p $players { lappend stacks "$p:[dict get $g stacks $p]" }
- 685 set stage [dict get $g stage]
- 686 set pot [dict get $g pot]
- 687 set comm [join [dict get $g community] { }]
- 688 if {$comm eq ""} { set comm "-" }
- 689 set actor [poker:actor $g]
- 690 putquick "PRIVMSG $target :Stage=$stage | Pot=$pot | Community=$comm | Stacks=[join $stacks ", "] | To act=$actor"
- 691}
- 692
- 693proc poker:send_hole_cards {nick target} {
- 694 global poker_games
- 695 if {![info exists poker_games($target)]} { return }
- 696 set g $poker_games($target)
- 697 if {![dict exists $g hands $nick]} {
- 698 putquick "PRIVMSG $nick :You have no hand (not in the current hand)."
- 699 return
- 700 }
- 701 set cards [dict get $g hands $nick]
- 702 putquick "PRIVMSG $nick :Your hole cards: [join $cards { }]"
- 703}
- 704
- 705proc poker:debug_state {nick target} {
- 706 global poker_games
- 707 if {![info exists poker_games($target)]} { putquick "PRIVMSG $target :No game."; return }
- 708 set g $poker_games($target)
- 709 putquick "PRIVMSG $target :DEBUG: [dict keys $g]"
- 710 putquick "PRIVMSG $target :DEBUG: $g"
- 711}
- 712
- 713# Charge a player's stack; returns true if fully charged, false if insufficient (no change to stacks)
- 714proc poker:charge_player {gameVar nick amount} {
- 715 upvar $gameVar g
- 716 set have [dict get $g stacks $nick]
- 717 if {$amount <= 0} { return 1 }
- 718 if {$have < $amount} { return 0 }
- 719 dict incr g stacks $nick [expr {-1 * $amount}]
- 720 return 1
- 721}
- 722
- 723proc poker:other_idx {g} {
- 724 set idx [dict get $g to_act_idx]
- 725 return [expr {1 - $idx}]
- 726}
- 727proc poker:previous_player {g} {
- 728 set idx [dict get $g to_act_idx]
- 729 return [lindex [dict get $g players] [expr {1 - $idx}]]
- 730}
- 731proc poker:other_player {g} {
- 732 return [lindex [dict get $g players] [expr {1 - [dict get $g to_act_idx]}]]
- 733}
- 734
- 735# ---- Deck ----
- 736proc poker:build_deck {} {
- 737 set suits {♠ ♥ ♦ ♣}
- 738 set ranks {2 3 4 5 6 7 8 9 10 J Q K A}
- 739 set deck {}
- 740 foreach s $suits { foreach r $ranks { lappend deck "${r}$s" } }
- 741 return $deck
- 742}
- 743proc poker:draw_one {deckVar} {
- 744 upvar $deckVar deck
- 745 set n [llength $deck]
- 746 if {$n <= 0} { return "" }
- 747 set idx [expr {int(rand()*$n)}]
- 748 set card [lindex $deck $idx]
- 749 set deck [lreplace $deck $idx $idx]
- 750 return $card
- 751}
- 752
- 753# ---- Showdown & hand evaluation ----
- 754proc poker:showdown {target} {
- 755 global poker_games
- 756 set g $poker_games($target)
- 757 set p1 [lindex [dict get $g players] 0]
- 758 set p2 [lindex [dict get $g players] 1]
- 759 set c [dict get $g community]
- 760 set h1 [dict get $g hands $p1]
- 761 set h2 [dict get $g hands $p2]
- 762 set pot [dict get $g pot]
- 763
- 764 putquick "PRIVMSG $target :Showdown!"
- 765 putquick "PRIVMSG $target :Community: [join $c { }]"
- 766 putquick "PRIVMSG $target :$p1 shows: [join $h1 { }]"
- 767 putquick "PRIVMSG $target :$p2 shows: [join $h2 { }]"
- 768
- 769 set score1 [poker:eval7 [concat $h1 $c]]
- 770 set score2 [poker:eval7 [concat $h2 $c]]
- 771
- 772 set cmp [poker:compare_scores $score1 $score2]
- 773 if {$cmp > 0} {
- 774 dict incr g stacks $p1 $pot
- 775 putquick "PRIVMSG $target :$p1 wins $pot with [lindex $score1 end]!"
- 776 } elseif {$cmp < 0} {
- 777 dict incr g stacks $p2 $pot
- 778 putquick "PRIVMSG $target :$p2 wins $pot with [lindex $score2 end]!"
- 779 } else {
- 780 set half [expr {$pot/2}]
- 781 set rem [expr {$pot - $half}]
- 782 dict incr g stacks $p1 $half
- 783 dict incr g stacks $p2 $rem
- 784 putquick "PRIVMSG $target :Split pot $pot — both tie!"
- 785 }
- 786
- 787 dict set g pot 0
- 788 dict set g stage "lobby"
- 789 dict set g current_bet 0
- 790 dict set g to_act_idx -1
- 791 dict set g community {}
- 792 dict set g hands [dict create]
- 793 set poker_games($target) $g
- 794}
- 795
- 796# Convert rank string to numeric
- 797proc poker:rankval {r} {
- 798 switch -nocase -- $r {
- 799 A { return 14 }
- 800 K { return 13 }
- 801 Q { return 12 }
- 802 J { return 11 }
- 803 10 { return 10 }
- 804 default {
- 805 if ([string is integer -strict $r]) { return $r }
- 806 return 0
- 807 }
- 808 }
- 809}
- 810# Parse "10♣" -> {10 ♣}
- 811proc poker:split_card {card} {
- 812 set suit [string index $card end]
- 813 set rank [string range $card 0 end-1]
- 814 return [list $rank $suit]
- 815}
- 816# Highest straight from list of ranks (unique, numeric). Returns top rank or 0; handles wheel (A-5).
- 817proc poker:highest_straight {ranks} {
- 818 if {![llength $ranks]} { return 0 }
- 819 set u [lsort -integer -unique $ranks]
- 820 if {[lsearch -exact $u 14] >= 0} { lappend u 1 }
- 821 set best 0
- 822 set run 1
- 823 for {set i 1} {$i < [llength $u]} {incr i} {
- 824 set prev [lindex $u [expr {$i-1}]]
- 825 set curr [lindex $u $i]
- 826 if {$curr == $prev+1} {
- 827 incr run
- 828 if {$run >= 5} { set best $curr }
- 829 } elseif {$curr != $prev} {
- 830 set run 1
- 831 }
- 832 }
- 833 return $best
- 834}
- 835# Evaluate 7 cards: return score list {cat t1 t2 ... label}
- 836# cat order: 8=StraightFlush, 7=Four, 6=FullHouse, 5=Flush, 4=Straight, 3=Trips, 2=TwoPair, 1=Pair, 0=High
- 837proc poker:eval7 {cards} {
- 838 set ranks {}
- 839 set suits {}
- 840 foreach c $cards {
- 841 lassign [poker:split_card $c] rc sc
- 842 lappend ranks [poker:rankval $rc]
- 843 lappend suits $sc
- 844 }
- 845 array set rcount {}
- 846 foreach r $ranks { incr rcount($r) }
- 847 array set scount {}
- 848 foreach s $suits { incr scount($s) }
- 849
- 850 set flushSuit ""
- 851 foreach s [array names scount] { if {$scount($s) >= 5} { set flushSuit $s; break } }
- 852
- 853 set straightHi [poker:highest_straight $ranks]
- 854 set sflushHi 0
- 855 if {$flushSuit ne ""} {
- 856 set rflush {}
- 857 for {set i 0} {$i < [llength $cards]} {incr i} {
- 858 if {[lindex $suits $i] eq $flushSuit} { lappend rflush [lindex $ranks $i] }
- 859 }
- 860 set sflushHi [poker:highest_straight $rflush]
- 861 }
- 862
- 863 set quads {}
- 864 set trips {}
- 865 set pairs {}
- 866 set singles {}
- 867 foreach r [lsort -integer -decreasing -unique $ranks] {
- 868 set c $rcount($r)
- 869 if {$c == 4} { lappend quads $r
- 870 } elseif {$c == 3} { lappend trips $r
- 871 } elseif {$c == 2} { lappend pairs $r
- 872 } else { lappend singles $r }
- 873 }
- 874
- 875 if {$sflushHi > 0} { return [list 8 $sflushHi "Straight Flush ($sflushHi-high)"] }
- 876 if {[llength $quads]} {
- 877 set k [lindex $quads 0]
- 878 set kick [lindex $singles 0]
- 879 if {$kick eq ""} { set kick [lindex $pairs 0] }
- 880 return [list 7 $k $kick "Four of a Kind ($k)"]
- 881 }
- 882 if {[llength $trips] >= 1 && ([llength $pairs] >= 1 || [llength $trips] >= 2)} {
- 883 set t [lindex $trips 0]
- 884 set p ""
- 885 if {[llength $pairs] >= 1} {
- 886 set p [lindex $pairs 0]
- 887 } elseif {[llength $trips] >= 2} {
- 888 set p [lindex $trips 1]
- 889 }
- 890 return [list 6 $t $p "Full House ($t over $p)"]
- 891 }
- 892 if {$flushSuit ne ""} {
- 893 set rflush {}
- 894 for {set i 0} {$i < [llength $cards]} {incr i} {
- 895 if {[lindex $suits $i] eq $flushSuit} { lappend rflush [lindex $ranks $i] }
- 896 }
- 897 set top [lrange [lsort -integer -decreasing -unique $rflush] 0 4]
- 898 return [concat [list 5] $top [list "Flush ([join $top , ])"]]
- 899 }
- 900 if {$straightHi > 0} { return [list 4 $straightHi "Straight ($straightHi-high)"] }
- 901 if {[llength $trips]} {
- 902 set t [lindex $trips 0]
- 903 set kicks [lrange [lsort -integer -decreasing -unique $singles] 0 1]
- 904 return [concat [list 3 $t] $kicks [list "Three of a Kind ($t)"]]
- 905 }
- 906 if {[llength $pairs] >= 2} {
- 907 set p1 [lindex $pairs 0]
- 908 set p2 [lindex $pairs 1]
- 909 set kick [lindex [lsort -integer -decreasing -unique $singles] 0]
- 910 return [list 2 $p1 $p2 $kick "Two Pair ($p1 & $p2)"]
- 911 }
- 912 if {[llength $pairs] >= 1} {
- 913 set p1 [lindex $pairs 0]
- 914 set kicks [lrange [lsort -integer -decreasing -unique $singles] 0 2]
- 915 return [concat [list 1 $p1] $kicks [list "Pair of $p1s"]]
- 916 }
- 917 set top5 [lrange [lsort -integer -decreasing -unique $ranks] 0 4]
- 918 return [concat [list 0] $top5 [list "High Card ([lindex $top5 0])"]]
- 919}
- 920proc poker:compare_scores {s1 s2} {
- 921 set n [expr {[llength $s1] < [llength $s2] ? [llength $s1] : [llength $s2]}]
- 922 for {set i 0} {$i < [expr {$n-1}]} {incr i} {
- 923 set a [lindex $s1 $i]
- 924 set b [lindex $s2 $i]
- 925 if {$a > $b} { return 1 }
- 926 if {$a < $b} { return -1 }
- 927 }
- 928 return 0
- 929}
- 930
- 931# ---- Misc ----
- 932proc lremove {list item} {
- 933 set idx [lsearch $list $item]
- 934 if {$idx >= 0} { return [lreplace $list $idx $idx] }
- 935 return $list
- 936}
- 937proc poker:send_grok_comment {target comment} {
- 938 global ai
- 939 set encoded [::http::formatQuery message $comment nick "Grok"]
- 940 set url "$ai(link)?$encoded"
- 941 if {[catch {::http::geturl $url -timeout 30000 -command [list ai:callback $target "Grok"] -headers {Accept application/json}} err]} {
- 942 putquick "PRIVMSG $target :$comment"
- 943 }
- 944}
- 945
- 946putlog "grok.tcl loaded"
Raw Paste