]> Pileus Git - ~andy/rhawk/blob - spades.awk
Allow stats on previous games
[~andy/rhawk] / spades.awk
1 # For saving
2 @include "json.awk"
3
4 # Functions
5 function sp_init(cards, tmp0, tmp1)
6 {
7         # Init deck
8         cards ="As Ks Qs Js 10s 9s 8s 7s 6s 5s 4s 3s 2s "\
9                "Ah Kh Qh Jh 10h 9h 8h 7h 6h 5h 4h 3h 2h "\
10                "Ac Kc Qc Jc 10c 9c 8c 7c 6c 5c 4c 3c 2c "\
11                "Ad Kd Qd Jd 10d 9d 8d 7d 6d 5d 4d 3d 2d"
12         split(cards, tmp0)
13         for (i=1; i<=length(tmp0); i++)
14                 sp_deck[tmp0[i]] = i
15 }
16
17 function sp_reset(type)
18 {
19         # Per message
20         if (type <  0) {
21                 sp_from     = ""    #    The speakers player name
22                 sp_valid    = ""    #    It is the speaker turn
23         }
24
25         # Per hand
26         if (type >= 0) {
27                 sp_suit     = ""    #     The lead suit {s,h,d,c}
28                 sp_piles    = ""    # [x] Played cards this turn
29                 delete sp_pile      # [x] Played cards this turn
30         }
31
32         # Per round
33         if (type >= 1) {
34                 sp_state    = "bid" #     {new,join,bid,pass,play}
35                 sp_broken   = 0     #     Whether spades are broken
36                 delete sp_last      # [x] The result of the last hand
37                 delete sp_hands     # [p] Each players cards
38                 delete sp_looked    # [i] Whether a player has looked a their cards
39                 delete sp_bids      # [i] Each players bid
40                 delete sp_nil       # [i] Nil multiplier 0=regular, 1=nil, 2=blind
41                 delete sp_pass      # [i] Cards to pass
42                 delete sp_tricks    # [i] Tricks this round
43         }
44
45         # Per game
46         if (type >= 2) {
47                 sp_state    = "new" #     {new,join,bid,pass,play}
48                 sp_owner    = ""    #     Who started the game
49                 sp_playto   = 0     #     Score the game will go to
50                 sp_dealer   =-1     #     Who is dealing this round
51                 sp_turn     = 0     #     Index of who's turn it is
52                 sp_player   = ""    #     Who's turn it is
53                 sp_limit    = 10    #     Bag out limit / nil bonus
54                 delete sp_players   # [p] Player names players["name"] -> i
55                 delete sp_auths     # [c] Player auth names auths["auth"] -> "name"
56                 delete sp_share     # [c] Player teammates share["friend"] -> "name"
57                 delete sp_order     # [i] Player order order[i] -> "name"
58                 delete sp_scores    # [i] Teams score
59         }
60
61         # Persistent
62         if (type >= 3) {
63                 sp_channel  = ""    #     channel to play in
64                 sp_log      = ""    #     Log file name
65                 delete sp_notify    # [p] E-mail notification address
66         }
67 }
68
69 function sp_acopy(dst, src,     key)
70 {
71         if (isarray(src)) {
72                 delete(dst)
73                 for (key in src)
74                         json_copy(dst, key, src[key])
75         }
76 }
77
78 function sp_save(file,  game)
79 {
80         # Per hand
81         game["suit"]    = sp_suit;
82         game["piles"]   = sp_piles;
83         json_copy(game, "pile",    sp_pile);
84
85         # Per round
86         game["state"]   = sp_state;
87         game["broken"]  = sp_broken;
88         json_copy(game, "last",    sp_last);
89         json_copy(game, "looked",  sp_looked);
90         json_copy(game, "bids",    sp_bids);
91         json_copy(game, "nil",     sp_nil);
92         json_copy(game, "pass",    sp_pass);
93         json_copy(game, "tricks",  sp_tricks);
94
95         # Per game
96         game["owner"]   = sp_owner;
97         game["playto"]  = sp_playto;
98         game["dealer"]  = sp_dealer;
99         game["turn"]    = sp_turn;
100         game["player"]  = sp_player;
101         game["limit"]   = sp_limit;
102         json_copy(game, "hands",   sp_hands);
103         json_copy(game, "players", sp_players);
104         json_copy(game, "auths",   sp_auths);
105         json_copy(game, "share",   sp_share);
106         json_copy(game, "order",   sp_order);
107         json_copy(game, "scores",  sp_scores);
108
109         # Persistent
110         game["channel"] = sp_channel;
111         game["log"]     = sp_log;
112         json_copy(game, "notify",  sp_notify);
113
114         # Save
115         json_save(file, game);
116 }
117
118 function sp_load(file,  game)
119 {
120         # Load
121         if (!json_load(file, game))
122                 return
123
124         # Per hand
125         sp_suit    = game["suit"];
126         sp_piles   = game["piles"];
127         sp_acopy(sp_pile,    game["pile"]);
128
129         # Per round
130         sp_state   = game["state"];
131         sp_broken  = game["broken"];
132         sp_acopy(sp_last,    game["last"]);
133         sp_acopy(sp_looked,  game["looked"]);
134         sp_acopy(sp_bids,    game["bids"]);
135         sp_acopy(sp_nil,     game["nil"]);
136         sp_acopy(sp_pass,    game["pass"]);
137         sp_acopy(sp_tricks,  game["tricks"]);
138
139         # Per game
140         sp_owner   = game["owner"];
141         sp_playto  = game["playto"];
142         sp_dealer  = game["dealer"];
143         sp_turn    = game["turn"];
144         sp_player  = game["player"];
145         sp_limit   = game["limit"];
146         sp_acopy(sp_hands,   game["hands"]);
147         sp_acopy(sp_players, game["players"]);
148         sp_acopy(sp_auths,   game["auths"]);
149         sp_acopy(sp_share,   game["share"]);
150         sp_acopy(sp_order,   game["order"]);
151         sp_acopy(sp_scores,  game["scores"]);
152
153         # Persistent
154         sp_channel = game["channel"];
155         sp_log     = game["log"];
156         sp_acopy(sp_notify,  game["notify"]);
157 }
158
159 function sp_say(msg)
160 {
161         print strftime("%Y-%m-%d %H:%M:%S | ") msg >> "logs/" sp_log
162         fflush("logs/" sp_log)
163         say(sp_channel, msg);
164 }
165
166 function sp_pretty(cards, who)
167 {
168         if (!nocolor[who]) {
169                 gsub(/[0-9JQKA]*[sc]/, "\0031,00\002&\017", cards) # black
170                 gsub(/[0-9JQKA]*[hd]/, "\0034,00\002&\017", cards) # red
171         }
172         if (!nounicode[who]) {
173                 gsub(/s/, "\002♠", cards)
174                 gsub(/h/, "\002♥", cards)
175                 gsub(/d/, "\002♦", cards)
176                 gsub(/c/, "\002♣", cards)
177         }
178         return cards
179 }
180
181 function sp_next(who, prev)
182 {
183         prev      = sp_turn
184         sp_turn   = who ? sp_players[who] : (sp_turn + 1) % 4
185         if (length(sp_order) == 4)
186                 sp_player = sp_order[sp_turn]
187         return prev
188 }
189
190 function sp_shuf(i, mixed)
191 {
192         sp_usort(sp_players, mixed)
193         for (i in mixed) {
194                 sp_order[i-1] = mixed[i]
195                 sp_players[mixed[i]] = i-1
196         }
197 }
198
199 function sp_deal(       shuf)
200 {
201         sp_say("/me deals the cards")
202         sp_usort(sp_deck, shuf)
203         for (i=1; i<=52; i++)
204                 sp_hands[sp_order[i%4]][shuf[i]] = 1
205         sp_state  = "bid"
206         sp_dealer = (sp_dealer+1)%4
207         sp_turn   =  sp_dealer
208         sp_player =  sp_order[sp_turn]
209         sp_say(sp_player ": you bid first!")
210 }
211
212 function sp_hand(to, who,       sort, str)
213 {
214         asorti(sp_hands[who], sort, "sp_csort")
215         for (i=0; i<length(sort); i++)
216                 str = str "" sprintf("%4s", sort[i])
217         gsub(/^ +| +$/, "", str)
218         return sp_pretty(str, to)
219 }
220
221 function sp_hasa(who, expr)
222 {
223         for (c in sp_hands[who]) {
224                 if (c ~ expr)
225                         return 1
226         }
227         return 0
228 }
229
230 function sp_type(card)
231 {
232         return substr(card, length(card))
233 }
234
235 function sp_usort(list, out) {
236         for (i in list)
237                 out[i] = rand()
238         asorti(out, out, "@val_num_asc")
239 }
240
241 function sp_csort(i1,v1,i2,v2) {
242         return sp_deck[i1] > sp_deck[i2] ? +1 :
243                sp_deck[i1] < sp_deck[i2] ? -1 : 0;
244 }
245
246 function sp_winner(     card, tmp)
247 {
248         for (card in sp_pile)
249                 if (card !~ sp_suit && card !~ /s/)
250                         delete sp_pile[card]
251         asorti(sp_pile, tmp, "sp_csort")
252         #print "pile: " tmp[1] ">" tmp[2] ">" tmp[3] ">" tmp[4]
253         return tmp[1]
254 }
255
256 function sp_team(i)
257 {
258         #return "{" sp_order[i+0] "," sp_order[i+2] "}"
259         return sp_order[i+0] "/" sp_order[i+2]
260 }
261
262 function sp_bags(i,     bags)
263 {
264         bags = sp_scores[i] % sp_limit
265         if (bags < 0)
266                 bags += sp_limit
267         return bags
268 }
269
270 function sp_bid(who)
271 {
272         return sp_nil[who] == 0 ? sp_bids[who] :
273                sp_nil[who] == 1 ? "nil"        :
274                sp_nil[who] == 2 ? "blind"      : "n/a"
275 }
276
277 function sp_passer(who)
278 {
279         return sp_nil[(who+0)%4] == 2 || sp_nil[(who+1)%4] != 0 ||
280                sp_nil[(who+2)%4] == 2 || sp_nil[(who+3)%4] != 0
281 }
282
283 function sp_bidders(    i, turn, bid, bids)
284 {
285         for (i = 0; i < 4; i++) {
286                 turn = (sp_dealer + i) % 4
287                 if (bid = sp_bid(turn))
288                         bids = bids " " sp_order[turn] ":" bid
289         }
290         gsub(/^ +| +$/, "", bids)
291         return bids
292 }
293
294 function sp_score(      bids, times, tricks)
295 {
296         for (i=0; i<2; i++) {
297                 bids   = sp_bids[i]   + sp_bids[i+2]
298                 tricks = sp_tricks[i] + sp_tricks[i+2]
299                 bags   = tricks - bids
300                 times  = int((sp_bags(i) + bags) / sp_limit)
301                 if (times > 0) {
302                         sp_say(sp_team(i) " bag" (times>1?" way ":" ") "out")
303                         sp_scores[i] -= sp_limit * 10 * times;
304                 }
305                 if (tricks >= bids) {
306                         sp_say(sp_team(i) " make their bid: " tricks "/" bids)
307                         sp_scores[i] += bids*10 + bags;
308                 } else {
309                         sp_say(sp_team(i) " go bust: " tricks "/" bids)
310                         sp_scores[i] -= bids*10;
311                 }
312         }
313         for (i=0; i<4; i++) {
314                 if (!sp_nil[i])
315                         continue
316                 sp_say(sp_order[i] " " \
317                     (sp_nil[i] == 1 && !sp_tricks[i] ? "makes nil!"       :
318                      sp_nil[i] == 1 &&  sp_tricks[i] ? "fails at nil!"    :
319                      sp_nil[i] == 2 && !sp_tricks[i] ? "makes blind nil!" :
320                      sp_nil[i] == 2 &&  sp_tricks[i] ? "fails miserably at blind nil!" :
321                                                        "unknown"))
322                 sp_scores[i%2] += sp_limit * 10 * sp_nil[i] * \
323                         (sp_tricks[i] == 0 ? 1 : -1)
324         }
325         if (sp_scores[0] > sp_scores[1])
326                 sp_say(sp_team(0) " lead " sp_scores[0] " to " sp_scores[1] " of " sp_playto)
327         else if (sp_scores[1] > sp_scores[0])
328                 sp_say(sp_team(1) " lead " sp_scores[1] " to " sp_scores[0] " of " sp_playto)
329         else
330                 sp_say("tied at " sp_scores[0] " of " sp_playto)
331 }
332
333 function sp_play(card,  winner, pi)
334 {
335         delete sp_hands[sp_from][card]
336         sp_pile[card] = sp_player
337         sp_piles      = sp_piles (sp_piles?",":"") card
338         sp_next()
339
340         if (card ~ /s/)
341                 sp_broken = 1
342
343         # Start hand
344         if (length(sp_pile) == 1)
345                 sp_suit = sp_type(card)
346
347         # Finish hand
348         if (length(sp_pile) == 4) {
349                 winner = sp_winner()
350                 pi     = sp_players[sp_pile[winner]]
351                 sp_tricks[pi]++
352                 sp_say(sp_pile[winner] " wins with " sp_pretty(winner, FROM) \
353                        " (" sp_pretty(sp_piles, FROM) ")")
354                 sp_last["player"] = sp_pile[winner];
355                 sp_last["pile"]   = sp_piles;
356                 sp_next(sp_pile[winner])
357                 sp_reset(0)
358         }
359
360         # Finish round
361         if (sp_tricks[0] + sp_tricks[1] + \
362             sp_tricks[2] + sp_tricks[3] == 13) {
363                 sp_say("Round over!")
364                 sp_score()
365                 if (sp_scores[0] >= sp_playto || sp_scores[1] >= sp_playto &&
366                     sp_scores[0]              != sp_scores[1]) {
367                         sp_say("Game over!")
368                         winner = sp_scores[0] > sp_scores[1] ? 0 : 1
369                         looser = !winner
370                         say(CHANNEL, sp_team(winner) " wins the game " \
371                             sp_scores[winner] " to " sp_scores[looser])
372                         say(CHANNEL, sp_order[winner+0] "++")
373                         say(CHANNEL, sp_order[winner+2] "++")
374                         sp_reset(2)
375
376                 } else {
377                         if (sp_scores[0] == sp_scores[1] && 
378                             sp_scores[0] >= sp_playto)
379                                 sp_say("It's tie! Playing an extra round!");
380                         sp_reset(1)
381                         sp_deal()
382                 }
383         }
384 }
385
386 # Statistics
387 function sp_delay(sec)
388 {
389         return (sec > 60*60*24 ? int(sec/60/60/24) "d " : "") \
390                (sec > 60*60    ? int(sec/60/60)%24 "h " : "") \
391                                  int(sec/60)%60    "m"      
392 }
393
394 function sp_max(list,    i, max)
395 {
396         for (i=0; i<length(list); i++)
397                 if (max == "" || list[i] > max)
398                         max = list[i]
399         return max
400 }
401
402 function sp_avg(list,    i, sum)
403 {
404         for (i=0; i<length(list); i++)
405                 sum += list[i]
406         return sum / length(list)
407 }
408
409 function sp_stats(file,   line, arr, time, user, turn, start, delay)
410 {
411         # Process log file
412         while ((stat = getline line < file) > 0) {
413                 # Parse date
414                 if (!match(line, /^([0-9\- \:]*) \| (.*)$/, arr))
415                         continue
416                 gsub(/[:-]/, " ", arr[1])
417                 time = mktime(arr[1])
418
419                 # Parse user
420                 if (!match(arr[2], /^([^:]*): (.*)$/, arr))
421                         continue
422                 user = arr[1]
423
424                 # Record user latency
425                 if (turn) {
426                         delay[turn][length(delay[turn])] = time - start
427                         turn  = 0
428                 }
429                 if (match(arr[2], /^it is your.*$/, arr)) {
430                         turn  = user
431                         start = time
432                 } 
433         }
434         close(file)
435
436         # Check for error
437         if (stat < 0)
438                 reply("File does not exist: " file);
439
440         # Output statistics
441         for (user in delay) {
442                 say("latency for " user \
443                         ": " sp_delay(sp_avg(delay[user])) " (avg)" \
444                         ", " sp_delay(sp_max(delay[user])) " (max)")
445         }
446 }
447
448 # Misc
449 BEGIN {
450         cmd = "od -An -N4 -td4 /dev/random"
451         cmd | getline seed
452         close(cmd)
453         srand(seed)
454         sp_init()
455         sp_reset(2)
456         sp_load("var/sp_cur.json");
457         #if (sp_channel)
458         #       sp_say("Game restored.")
459 }
460
461 // {
462         sp_from  = AUTH in sp_auths ? sp_auths[AUTH] : \
463                    AUTH in sp_share ? sp_share[AUTH] : FROM
464         sp_valid = sp_from && sp_from == sp_player
465 }
466
467 CMD == "PRIVMSG" &&
468 ! /help/ &&
469 /[Ss]pades/ {
470         say("Spades! " sp_pretty("As,Ah,Ad,Ac", FROM))
471 }
472
473 AUTH == OWNER &&
474 /^\.savegame/ {
475         sp_save("var/sp_save.json");
476         say("Game saved.")
477 }
478
479 AUTH == OWNER &&
480 /^\.loadgame/ {
481         sp_load("var/sp_save.json");
482         say("Game loaded.")
483 }
484
485 # Help
486 /^\.help$/ {
487         say(".help spades -- play a game of spades")
488 }
489
490 /^\.help [Ss]pades$/ {
491         say("Spades -- play a game of spades")
492         say(".help game -- setup and administer the game")
493         say(".help play -- commands for playing spades")
494         say(".help auth -- control player authorization")
495         next
496 }
497
498 /^\.help game$/ {
499         say(".newgame [score] -- start a game to <score> points, default 500")
500         say(".endgame -- abort the current game")
501         say(".savegame -- save the current game to disk")
502         say(".loadgame -- load the previously saved game")
503         next
504 }
505
506 /^\.help play$/ {
507         say(".join -- join the current game")
508         say(".look -- look at your cards")
509         say(".bid [n] -- bid for <n> tricks")
510         say(".pass [card] -- pass a card to your partner")
511         say(".play [card] -- play a card")
512         say(".last -- show who took the previous trick")
513         say(".turn -- check whose turn it is")
514         say(".bids -- check what everyone bid")
515         say(".tricks -- check how many trick have been taken")
516         say(".score -- check the score")
517         next
518 }
519
520 /^\.help auth$/ {
521         say(".auth [who] -- display authentication info for a user")
522         say(".allow [who] -- allow another person to play on your behalf")
523         say(".deny [who] -- prevent a previously allowed user from playing")
524         say(".show -- display which users can play for which players")
525         say(".notify [addr] -- email user when it is their turn")
526         next
527 }
528
529 # Debugging
530 AUTH == OWNER &&
531 /^\.deal (\w+) (.*)/ {
532         sp_say(FROM " is cheating for " $2)
533         delete sp_hands[$2]
534         for (i=3; i<=NF; i++)
535                 sp_hands[$2][$i] = 1
536         next
537 }
538
539 AUTH == OWNER &&
540 /^\.order (\w+) ([0-4])/ {
541         sp_say(FROM " is cheating for " $2)
542         sp_order[$3] = $2
543         sp_players[$2] = $3
544         sp_player = sp_order[sp_turn]
545 }
546
547 AUTH == OWNER &&
548 sp_state == "play" &&
549 /^\.force (\w+) (\S+)$/ {
550         sp_say(FROM " is cheating for " $2)
551         sp_from = $2
552         sp_play($3)
553         next
554 }
555
556
557 # Setup
558 match($0, /^\.newgame ?([1-9][0-9]*) *- *([1-9][0-9]*)$/, _arr) {
559         if (_arr[2] > _arr[1])
560                 $0 = $1 " " int(rand() * (_arr[2]-_arr[1])+_arr[1])
561 }
562
563 /^\.newgame ?([1-9][0-9]*)?$/ {
564         if (sp_state != "new") {
565                 reply("There is already a game in progress.")
566         } else {
567                 $1         = ".join"
568                 sp_owner   = FROM
569                 sp_playto  = $2 ? $2 : 200
570                 sp_limit   = sp_playto > 200 ? 10 : 5;
571                 sp_state   = "join"
572                 sp_channel = DST
573                 sp_log     = strftime("%Y%m%d_%H%M%S.log")
574                 sp_say(sp_owner " starts a game of Spades to " sp_playto " with " sp_limit " bags!")
575         }
576 }
577
578 (sp_from == sp_owner || AUTH == OWNER) &&
579 /^\.endgame$/ {
580         if (sp_state == "new") {
581                 reply("There is no game in progress.")
582         } else {
583                 sp_say(FROM " ends the game")
584                 sp_reset(2)
585         }
586 }
587
588 /^\.join/ {
589         if (sp_state == "new") {
590                 reply("There is no game in progress")
591         }
592         else if (sp_state == "play") {
593                 reply("The game has already started")
594         }
595         else if (sp_state == "join" && sp_from in sp_players) {
596                 reply("You are already playing")
597         }
598         else if (sp_state == "join") {
599                 i = sp_next()
600                 sp_players[FROM] = i
601                 if (AUTH)
602                         sp_auths[AUTH] = FROM
603                 sp_order[i] = FROM
604                 sp_say(FROM " joins the game!")
605         }
606         if (sp_state == "join" && sp_turn == 0) {
607                 sp_shuf()
608                 sp_deal()
609         }
610 }
611
612 /^\.allow \S+$/ {
613         _who = $2 in USERS ? USERS[$2]["auth"] : ""
614         _str = _who && _who != $2 ? $2 " (" _who ")" : $2
615         if (sp_state ~ "new|join") {
616                 reply("The game has not yet started")
617         }
618         else if (!(sp_from in sp_players)) {
619                 reply("You are not playing")
620         }
621         else if (!_who) {
622                 reply(_str " is not logged in")
623         }
624         else if (_who in sp_players || _who in sp_auths) {
625                 reply(_str " is a primary player")
626         }
627         else if (_who in sp_share) {
628                 reply(_str " is already playing for " sp_share[_who])
629         }
630         else {
631                 sp_say(_str " can now play for " sp_from)
632                 sp_share[_who] = sp_from
633         }
634 }
635
636 /^\.deny \S+$/ {
637         _who = $2 in USERS ? USERS[$2]["auth"] : $2
638         _str = _who && _who != $2 ? $2 " (" _who ")" : $2
639         if (sp_state ~ "new|join") {
640                 reply("The game has not yet started")
641         }
642         else if (!(sp_from in sp_players)) {
643                 reply("You are not playing")
644         }
645         else if (_who in sp_players || _who in sp_auths) {
646                 reply(_str " is a primary player")
647         }
648         else if (!(_who in sp_share) || sp_share[_who] != sp_from) {
649                 reply(_str " is not playing for " sp_from)
650         }
651         else {
652                 sp_say(_str " can no longer play for " sp_from)
653                 delete sp_share[_who]
654         }
655 }
656
657 /^\.notify$/ {
658         if (sp_from in sp_notify)
659                 reply("Your address is " sp_notify[sp_from])
660         else
661                 reply("Your address is not set")
662 }
663
664 /^\.notify clear$/ {
665         if (sp_from in sp_notify) {
666                 reply("Removing address " sp_notify[sp_from])
667                 delete sp_notify[sp_from]
668         } else {
669                 reply("Your address is not set")
670         }
671 }
672
673 /^\.notify \S+@\S+.\S+$/ {
674         _addr = $2
675         gsub(/[^a-zA-Z0-9_+@.-]/, "", _addr)
676         sp_notify[sp_from] = _addr
677         reply("Notifying you at " _addr)
678 }
679
680 sp_state ~ "(bid|pass|play)" &&
681 /^\.show/ {
682         delete _lines
683         for (_i in sp_share)
684                 _lines[sp_share[_i]] = _lines[sp_share[_i]] " " _i
685         for (_i in _lines)
686                 sp_say(_i " allowed:" _lines[_i])
687 }
688
689 !sp_valid &&
690 (sp_state == "bid" || sp_state == "play") &&
691 /^\.(bid|play)\>/ {
692         if (sp_from in sp_players)
693                 reply("It is not your turn.")
694         else
695                 reply("You are not playing.")
696 }
697
698 sp_valid &&
699 sp_state == "bid" &&
700 /^\.bid (0|[1-9][0-9]*)$/ {
701         if ($2 < 0 || $2 > 13) {
702                 reply("You can only bid from 0 to 13")
703         } else {
704                 i = sp_next()
705                 sp_bids[i] = $2
706                 if ($2 == 0 && !sp_looked[i]) {
707                         sp_say(FROM " goes blind nil!")
708                         sp_nil[i] = 2
709                 } else if ($2 == 0) {
710                         sp_say(FROM " goes nil!")
711                         sp_nil[i] = 1
712                 } else {
713                         sp_nil[i] = 0
714                 }
715                 if (sp_turn != sp_dealer) {
716                         sp_say(sp_player ": it is your bid! (" sp_bidders() ")")
717                 } else {
718                         for (p in sp_players)
719                                 say(p, "You have: " sp_hand(p, p))
720                         sp_state = "play"
721                         for (i=0; i<2; i++) {
722                                 if (sp_passer(i)) {
723                                         sp_say(sp_team(i) ": select a card to pass " \
724                                             "(/msg " NICK " .pass <card>)")
725                                         sp_state = "pass"
726                                 }
727                         }
728                         if (sp_state == "play")
729                                 sp_say(sp_player ": you have the opening lead!")
730                 }
731         }
732 }
733
734 sp_state == "pass" &&
735 /^\.pass (\S+)$/ {
736         _card = $2
737         _team = sp_from in sp_players ? sp_players[sp_from] % 2 : 0
738
739         # check validity and pass
740         if (!(sp_from in sp_players)) {
741                 reply("You are not playing.")
742         }
743         else if (!sp_passer(_team)) {
744                 reply("Your team did not go blind")
745         }
746         else if (sp_pass[sp_players[sp_from]]) {
747                 reply("You have already passed a card")
748         }
749         else if (!(_card in sp_deck)) {
750                 reply("Invalid card")
751         }
752         else if (!(_card in sp_hands[sp_from])) {
753                 reply("You do not have that card")
754         }
755         else {
756                 sp_pass[sp_players[sp_from]] = $2
757                 sp_say(FROM " passes a card")
758         }
759
760         # check for end of passing
761         if ((!sp_passer(0) || (sp_pass[0] && sp_pass[2])) &&
762             (!sp_passer(1) || (sp_pass[1] && sp_pass[3]))) {
763                 for (i in sp_pass) {
764                         _partner = (i+2)%4
765                         _card    = sp_pass[i]
766                         delete sp_hands[sp_order[i]][_card]
767                         sp_hands[sp_order[_partner]][_card] = 1
768                 }
769                 sp_say("Cards have been passed!")
770                 sp_say(sp_player ": you have the opening lead!")
771                 for (p in sp_players)
772                         say(p, "You have: " sp_hand(p, p))
773                 sp_state = "play"
774         }
775 }
776
777 sp_state ~ "(bid|pass|play)" &&
778 /^\.look$/ {
779         if (!(sp_from in sp_players)) {
780                 reply("You are not playing.")
781         } else {
782                 sp_looked[sp_players[sp_from]] = 1
783                 say(FROM, "You have: " sp_hand(FROM, sp_from))
784         }
785 }
786
787 sp_valid &&
788 sp_state == "play" &&
789 /^\.play (\S+)/ {
790         _card = $2
791         gsub(/[^A-Za-z0-9]/, "", _card);
792         if (!(_card in sp_deck)) {
793                 reply("Invalid card")
794         }
795         else if (sp_suit && _card !~ sp_suit && sp_hasa(sp_from, sp_suit)) {
796                 reply("You must follow suit (" sp_suit ")")
797         }
798         else if (_card ~ /s/ && length(sp_hands[sp_from]) == 13 && sp_hasa(sp_from, "[^s]$")) {
799                 reply("You cannot trump on the first hand")
800         }
801         else if (_card ~ /s/ && length(sp_pile) == 0 && sp_hasa(sp_from, "[^s]$") && !sp_broken) {
802                 reply("Spades have not been broken")
803         }
804         else if (!(_card in sp_hands[sp_from])) {
805                 reply("You do not have that card")
806         }
807         else {
808                 sp_play(_card)
809                 if (sp_state == "play") {
810                         if (length(sp_hands[sp_from]))
811                                 say(FROM, "You have: " sp_hand(FROM, sp_from))
812                         if (sp_piles)
813                                 sp_say(sp_player ": it is your turn! " \
814                                     "(" sp_pretty(sp_piles, sp_player) ")")
815                         else
816                                 sp_say(sp_player ": it is your turn!")
817                 }
818         }
819 }
820
821 /^\.last/ && sp_state == "play" {
822         if (!isarray(sp_last))
823                 say("No tricks have been taken!");
824         else
825                 say(sp_last["player"] " took " \
826                     sp_pretty(sp_last["pile"], FROM));
827 }
828
829 /^\.bids/ && sp_state == "bid" ||
830 /^\.turn/ && sp_state ~ "(bid|pass|play)" {
831         _bids  = sp_bidders()
832         _pile  = sp_pretty(sp_piles, FROM)
833         _extra = ""
834
835         for (_i in sp_share)
836                 if (/!/ && sp_share[_i] == sp_player)
837                         _extra = _extra " " _i "!"
838
839         if (sp_state == "bid" && !_bids)
840                 say("It is " sp_player "'s bid!" _extra)
841         if (sp_state == "bid" && _bids)
842                 say("It is " sp_player "'s bid!" _extra " (" _bids ")")
843         if (sp_state == "play" && !_pile)
844                 say("It is " sp_player "'s turn!" _extra)
845         if (sp_state == "play" && _pile)
846                 say("It is " sp_player "'s turn!" _extra " (" _pile ")")
847
848         for (_i=0; sp_state == "pass" && _i<4; _i++)
849                 if (sp_passer(_i) && !sp_pass[_i])
850                         say("Waiting for " sp_order[_i] " to pass a card!")
851
852         if (/!!/ && (sp_state == "bid" || sp_state == "play")) {
853                 if (sp_player in sp_notify) {
854                         _bids = _bids ? _bids    : "none"
855                         _pile = _pile ? sp_piles : "none"
856                         mail_send(sp_notify[sp_player],     \
857                                 "It is your " sp_state "!", \
858                                 "Bids so far:  " _bids "\n" \
859                                 "Cards played: " _pile)
860                         say("Notified " sp_player " at " sp_notify[sp_player])
861                 } else {
862                         say("No email address for " sp_player)
863                 }
864         }
865 }
866
867 /^\.bids$/ && sp_state ~ "(pass|play)" {
868         say(sp_order[0] " bid " sp_bid(0) ", " \
869             sp_order[2] " bid " sp_bid(2) ", " \
870             "total: " sp_bids[0] + sp_bids[2])
871         say(sp_order[1] " bid " sp_bid(1) ", " \
872             sp_order[3] " bid " sp_bid(3) ", " \
873             "total: " sp_bids[1] + sp_bids[3])
874 }
875
876 /^\.tricks$/ && sp_state == "play" {
877         say(sp_order[0] " took " int(sp_tricks[0]) "/" sp_bid(0) ", " \
878             sp_order[2] " took " int(sp_tricks[2]) "/" sp_bid(2))
879         say(sp_order[1] " took " int(sp_tricks[1]) "/" sp_bid(1) ", " \
880             sp_order[3] " took " int(sp_tricks[3]) "/" sp_bid(3))
881 }
882
883 (TO == NICK || DST == sp_channel) &&
884 /^\.(score|status)$/ {
885         if (sp_state == "new") {
886                 say("There is no game in progress")
887         }
888         if (sp_state ~ "join|bid|pass|play") {
889                 say("Playing to: " \
890                     sp_playto " points, " \
891                     sp_limit  " bags")
892         }
893         if (sp_state == "join") {
894                 say("Waiting for players: " \
895                     sp_order[0] " " sp_order[1] " " \
896                     sp_order[2] " " sp_order[3])
897         }
898         if (sp_state ~ "bid|pass|play") {
899                 say(sp_team(0) ": " \
900                     int(sp_scores[0]) " points, " \
901                     int(sp_bags(0))   " bags")
902                 say(sp_team(1) ": " \
903                     int(sp_scores[1]) " points, " \
904                     int(sp_bags(1))   " bags")
905         }
906 }
907
908 (TO == NICK || DST == sp_channel) &&
909 /^\.log/ {
910         say("http://pileus.org/andy/spades/" sp_log)
911 }
912
913 (TO == NICK || DST == sp_channel) &&
914 /^\.stats$/ {
915         sp_stats("logs/" sp_log);
916 }
917
918 (TO == NICK || DST == sp_channel) &&
919 /^\.stats ([0-9]+_[0-9]+)(\.log)$/ {
920         gsub(/\.log$/, "", $2);
921         sp_stats("logs/" $2 ".log");
922 }
923
924 /^\.((new|end|load)game|join|look|bid|pass|play|notify)/ {
925         sp_save("var/sp_cur.json");
926 }