Grok.tcl

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

Raw Paste

Comments 0
Login to join the discussion
  • No comments yet — be the first!
Login to post a comment. Login
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA), and — with your consent — to measure usage and show ads. See Privacy.