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