1 package server
2
3 import (
4 "bytes"
5 "fmt"
6 "log"
7 "strconv"
8 "strings"
9 "time"
10
11 "code.rocket9labs.com/tslocum/bgammon"
12 )
13
14 func (s *server) handleCommands() {
15 var cmd serverCommand
16 COMMANDS:
17 for cmd = range s.commands {
18 if cmd.client == nil {
19 log.Panicf("nil client with command %s", cmd.command)
20 } else if cmd.client.terminating || cmd.client.Terminated() {
21 continue
22 }
23
24 cmd.command = bytes.TrimSpace(cmd.command)
25
26 firstSpace := bytes.IndexByte(cmd.command, ' ')
27 var keyword string
28 var startParameters int
29 if firstSpace == -1 {
30 keyword = string(cmd.command)
31 startParameters = len(cmd.command)
32 } else {
33 keyword = string(cmd.command[:firstSpace])
34 startParameters = firstSpace + 1
35 }
36 if keyword == "" {
37 continue
38 }
39 keyword = strings.ToLower(keyword)
40 params := bytes.Fields(cmd.command[startParameters:])
41
42
43 if cmd.client.accountID == -1 {
44 resetCommand := keyword == bgammon.CommandResetPassword
45 if resetCommand {
46 if len(params) > 0 {
47 email := bytes.ToLower(bytes.TrimSpace(params[0]))
48 if len(email) > 0 {
49 err := resetAccount(s.mailServer, s.resetSalt, email)
50 if err != nil {
51 log.Fatalf("failed to reset password: %s", err)
52 }
53 }
54 }
55 cmd.client.Terminate("resetpasswordok")
56 continue
57 }
58
59 loginCommand := keyword == bgammon.CommandLogin || keyword == bgammon.CommandLoginJSON || keyword == "lj"
60 registerCommand := keyword == bgammon.CommandRegister || keyword == bgammon.CommandRegisterJSON || keyword == "rj"
61 if loginCommand || registerCommand {
62 if keyword == bgammon.CommandLoginJSON || keyword == bgammon.CommandRegisterJSON || keyword == "lj" || keyword == "rj" {
63 cmd.client.json = true
64 }
65
66 var username []byte
67 var password []byte
68 var randomUsername bool
69 if registerCommand {
70 sendUsage := func() {
71 cmd.client.Terminate("Please enter an email, username and password.")
72 }
73
74 var email []byte
75 if keyword == bgammon.CommandRegisterJSON || keyword == "rj" {
76 if len(params) < 4 {
77 sendUsage()
78 continue
79 }
80 email = params[1]
81 username = params[2]
82 password = bytes.Join(params[3:], []byte("_"))
83 } else {
84 if len(params) < 3 {
85 sendUsage()
86 continue
87 }
88 email = params[0]
89 username = params[1]
90 password = bytes.Join(params[2:], []byte("_"))
91 }
92 if onlyNumbers.Match(username) {
93 cmd.client.Terminate("Failed to register: Invalid username: must contain at least one non-numeric character.")
94 continue
95 }
96 password = bytes.ReplaceAll(password, []byte(" "), []byte("_"))
97 a := &account{
98 email: email,
99 username: username,
100 password: password,
101 }
102 err := registerAccount(s.passwordSalt, a)
103 if err != nil {
104 cmd.client.Terminate(fmt.Sprintf("Failed to register: %s", err))
105 continue
106 }
107 } else {
108 s.clientsLock.Lock()
109
110 readUsername := func() bool {
111 if cmd.client.json {
112 if len(params) > 1 {
113 username = params[1]
114 }
115 } else {
116 if len(params) > 0 {
117 username = params[0]
118 }
119 }
120 if len(bytes.TrimSpace(username)) == 0 {
121 username = s.randomUsername()
122 randomUsername = true
123 } else if !alphaNumericUnderscore.Match(username) {
124 cmd.client.Terminate("Invalid username: must contain only letters, numbers and underscores.")
125 return false
126 }
127 if onlyNumbers.Match(username) {
128 cmd.client.Terminate("Invalid username: must contain at least one non-numeric character.")
129 return false
130 } else if s.clientByUsername(username) != nil || s.clientByUsername(append([]byte("Guest_"), username...)) != nil || (!randomUsername && !s.nameAllowed(username)) {
131 cmd.client.Terminate("That username is already in use.")
132 return false
133 }
134 return true
135 }
136 if !readUsername() {
137 s.clientsLock.Unlock()
138 continue
139 }
140 if len(params) > 2 {
141 password = bytes.ReplaceAll(bytes.Join(params[2:], []byte(" ")), []byte(" "), []byte("_"))
142 }
143
144 s.clientsLock.Unlock()
145 }
146
147 if len(password) > 0 {
148 a, err := loginAccount(s.passwordSalt, username, password)
149 if err != nil {
150 cmd.client.Terminate(fmt.Sprintf("Failed to log in: %s", err))
151 continue
152 } else if a == nil {
153 cmd.client.Terminate("No account was found with the provided username and password. To log in as a guest, do not enter a password.")
154 continue
155 }
156
157 var name []byte
158 if bytes.HasPrefix(a.username, []byte("bot_")) {
159 name = append([]byte("BOT_"), a.username[4:]...)
160 } else {
161 name = a.username
162 }
163 if s.clientByUsername(name) != nil {
164 cmd.client.Terminate("That username is already in use.")
165 continue
166 }
167
168 cmd.client.account = a
169 cmd.client.accountID = a.id
170 cmd.client.name = name
171 cmd.client.autoplay = a.autoplay
172 } else {
173 cmd.client.accountID = 0
174 if !randomUsername && !bytes.HasPrefix(username, []byte("BOT_")) && !bytes.HasPrefix(username, []byte("Guest_")) {
175 username = append([]byte("Guest_"), username...)
176 }
177 cmd.client.name = username
178 }
179
180 cmd.client.sendEvent(&bgammon.EventWelcome{
181 PlayerName: string(cmd.client.name),
182 Clients: len(s.clients),
183 Games: len(s.games),
184 })
185
186 log.Printf("Client %d logged in as %s", cmd.client.id, cmd.client.name)
187
188
189 if cmd.client.account != nil {
190 a := cmd.client.account
191 cmd.client.sendEvent(&bgammon.EventSettings{
192 AutoPlay: a.autoplay,
193 Highlight: a.highlight,
194 Pips: a.pips,
195 Moves: a.moves,
196 Flip: a.flip,
197 Advanced: a.advanced,
198 Speed: a.speed,
199 })
200 }
201
202
203 s.gamesLock.RLock()
204 for _, g := range s.games {
205 if g.terminated() || g.Winner != 0 {
206 continue
207 }
208
209 var rejoin bool
210 if bytes.Equal(cmd.client.name, g.allowed1) {
211 rejoin = g.rejoin1
212 } else if bytes.Equal(cmd.client.name, g.allowed2) {
213 rejoin = g.rejoin2
214 }
215 if rejoin {
216 g.addClient(cmd.client)
217 cmd.client.sendNotice(fmt.Sprintf("Rejoined match: %s", g.name))
218 }
219 }
220 s.gamesLock.RUnlock()
221 continue
222 }
223
224 cmd.client.Terminate("You must login before using other commands.")
225 continue
226 }
227
228 clientGame := s.gameByClient(cmd.client)
229 if clientGame != nil && clientGame.client1 != cmd.client && clientGame.client2 != cmd.client {
230 switch keyword {
231 case bgammon.CommandHelp, "h", bgammon.CommandJSON, bgammon.CommandList, "ls", bgammon.CommandBoard, "b", bgammon.CommandLeave, "l", bgammon.CommandReplay, bgammon.CommandSet, bgammon.CommandDisconnect, bgammon.CommandPong:
232
233 default:
234 cmd.client.sendNotice("Command ignored: You are spectating this match.")
235 continue
236 }
237 }
238
239 switch keyword {
240 case bgammon.CommandHelp, "h":
241
242 cmd.client.sendEvent(&bgammon.EventHelp{
243 Topic: "",
244 Message: "Test help text",
245 })
246 case bgammon.CommandJSON:
247 sendUsage := func() {
248 cmd.client.sendNotice("To enable JSON formatted messages, send 'json on'. To disable JSON formatted messages, send 'json off'.")
249 }
250 if len(params) != 1 {
251 sendUsage()
252 continue
253 }
254 paramLower := strings.ToLower(string(params[0]))
255 switch paramLower {
256 case "on":
257 cmd.client.json = true
258 cmd.client.sendNotice("JSON formatted messages enabled.")
259 case "off":
260 cmd.client.json = false
261 cmd.client.sendNotice("JSON formatted messages disabled.")
262 default:
263 sendUsage()
264 }
265 case bgammon.CommandSay, "s":
266 if len(params) == 0 {
267 continue
268 }
269 if clientGame == nil {
270 cmd.client.sendNotice("Message not sent: You are not currently in a match.")
271 continue
272 }
273 opponent := clientGame.opponent(cmd.client)
274 if opponent == nil {
275 cmd.client.sendNotice("Message not sent: There is no one else in the match.")
276 continue
277 }
278 ev := &bgammon.EventSay{
279 Message: string(bytes.Join(params, []byte(" "))),
280 }
281 ev.Player = string(cmd.client.name)
282 opponent.sendEvent(ev)
283 if s.relayChat {
284 for _, spectator := range clientGame.spectators {
285 spectator.sendEvent(ev)
286 }
287 }
288 case bgammon.CommandList, "ls":
289 ev := &bgammon.EventList{}
290
291 s.gamesLock.RLock()
292 for _, g := range s.games {
293 listing := g.listing(cmd.client.name)
294 if listing == nil {
295 continue
296 }
297 ev.Games = append(ev.Games, *listing)
298 }
299 s.gamesLock.RUnlock()
300
301 cmd.client.sendEvent(ev)
302 case bgammon.CommandCreate, "c":
303 if clientGame != nil {
304 cmd.client.sendNotice("Failed to create match: Please leave the match you are in before creating another.")
305 continue
306 }
307
308 sendUsage := func() {
309 cmd.client.sendNotice("To create a public match please specify whether it is public or private, and also specify how many points are needed to win the match. When creating a private match, a password must also be provided.")
310 }
311 if len(params) < 2 {
312 sendUsage()
313 continue
314 }
315
316 var gamePassword []byte
317 gameType := bytes.ToLower(params[0])
318 var gameName []byte
319 var gamePoints []byte
320 switch {
321 case bytes.Equal(gameType, []byte("public")):
322 gamePoints = params[1]
323 if len(params) > 2 {
324 gameName = bytes.Join(params[2:], []byte(" "))
325 }
326 case bytes.Equal(gameType, []byte("private")):
327 if len(params) < 3 {
328 sendUsage()
329 continue
330 }
331 gamePassword = bytes.ReplaceAll(params[1], []byte("_"), []byte(" "))
332 gamePoints = params[2]
333 if len(params) > 3 {
334 gameName = bytes.Join(params[3:], []byte(" "))
335 }
336 default:
337 sendUsage()
338 continue
339 }
340
341 variant := bgammon.VariantBackgammon
342
343
344 variantNone := bytes.HasPrefix(gameName, []byte("0 ")) || bytes.Equal(gameName, []byte("0"))
345 variantAcey := bytes.HasPrefix(gameName, []byte("1 ")) || bytes.Equal(gameName, []byte("1"))
346 variantTabula := bytes.HasPrefix(gameName, []byte("2 ")) || bytes.Equal(gameName, []byte("2"))
347 if variantNone || variantAcey || variantTabula {
348 if variantAcey {
349 variant = bgammon.VariantAceyDeucey
350 } else if variantTabula {
351 variant = bgammon.VariantTabula
352 }
353 if len(gameName) > 1 {
354 gameName = gameName[2:]
355 } else {
356 gameName = nil
357 }
358 }
359
360 points, err := strconv.Atoi(string(gamePoints))
361 if err != nil || points < 1 || points > 99 {
362 sendUsage()
363 continue
364 }
365
366
367 if len(bytes.TrimSpace(gameName)) == 0 {
368 abbr := "'s"
369 lastLetter := cmd.client.name[len(cmd.client.name)-1]
370 if lastLetter == 's' || lastLetter == 'S' {
371 abbr = "'"
372 }
373 gameName = []byte(fmt.Sprintf("%s%s match", cmd.client.name, abbr))
374 }
375
376 g := newServerGame(<-s.newGameIDs, variant)
377 g.name = gameName
378 g.Points = int8(points)
379 g.password = gamePassword
380 g.addClient(cmd.client)
381
382 s.gamesLock.Lock()
383 s.games = append(s.games, g)
384 s.gamesLock.Unlock()
385
386 cmd.client.sendNotice(fmt.Sprintf("Created match: %s", g.name))
387
388 if len(g.password) == 0 {
389 cmd.client.sendNotice("Note: Please be patient as you wait for another player to join the match. A chime will sound when another player joins. While you wait, join the bgammon.org community via Discord, Matrix or IRC at bgammon.org/community")
390 }
391 case bgammon.CommandJoin, "j":
392 if clientGame != nil {
393 cmd.client.sendEvent(&bgammon.EventFailedJoin{
394 Reason: "Please leave the match you are in before joining another.",
395 })
396 continue
397 }
398
399 sendUsage := func() {
400 cmd.client.sendNotice("To join a match please specify its ID or the name of a player in the match. To join a private match, a password must also be specified.")
401 }
402
403 if len(params) == 0 {
404 sendUsage()
405 continue
406 }
407
408 var joinGameID int
409 if onlyNumbers.Match(params[0]) {
410 gameID, err := strconv.Atoi(string(params[0]))
411 if err == nil && gameID > 0 {
412 joinGameID = gameID
413 }
414
415 if joinGameID == 0 {
416 sendUsage()
417 continue
418 }
419 } else {
420 paramLower := bytes.ToLower(params[0])
421 s.clientsLock.Lock()
422 for _, sc := range s.clients {
423 if bytes.Equal(paramLower, bytes.ToLower(sc.name)) {
424 g := s.gameByClient(sc)
425 if g != nil {
426 joinGameID = g.id
427 }
428 break
429 }
430 }
431 s.clientsLock.Unlock()
432
433 if joinGameID == 0 {
434 cmd.client.sendEvent(&bgammon.EventFailedJoin{
435 Reason: "Match not found.",
436 })
437 continue
438 }
439 }
440
441 s.gamesLock.Lock()
442 for _, g := range s.games {
443 if g.terminated() {
444 continue
445 }
446 if g.id == joinGameID {
447 providedPassword := bytes.ReplaceAll(bytes.Join(params[1:], []byte(" ")), []byte("_"), []byte(" "))
448 if len(g.password) != 0 && (len(params) < 2 || !bytes.Equal(g.password, providedPassword)) {
449 cmd.client.sendEvent(&bgammon.EventFailedJoin{
450 Reason: "Invalid password.",
451 })
452 s.gamesLock.Unlock()
453 continue COMMANDS
454 }
455
456 if bytes.HasPrefix(bytes.ToLower(cmd.client.name), []byte("bot_")) && ((g.client1 != nil && !bytes.HasPrefix(bytes.ToLower(g.client1.name), []byte("bot_"))) || (g.client2 != nil && !bytes.HasPrefix(bytes.ToLower(g.client2.name), []byte("bot_")))) {
457 cmd.client.sendEvent(&bgammon.EventFailedJoin{
458 Reason: "Bots are not allowed to join player matches. Please create a match instead.",
459 })
460 continue COMMANDS
461 }
462
463 spectator := g.addClient(cmd.client)
464 s.gamesLock.Unlock()
465 cmd.client.sendNotice(fmt.Sprintf("Joined match: %s", g.name))
466 if spectator {
467 cmd.client.sendNotice("You are spectating this match. Chat messages are not relayed.")
468 }
469 continue COMMANDS
470 }
471 }
472 s.gamesLock.Unlock()
473
474 cmd.client.sendEvent(&bgammon.EventFailedJoin{
475 Reason: "Match not found.",
476 })
477 case bgammon.CommandLeave, "l":
478 if clientGame == nil {
479 cmd.client.sendEvent(&bgammon.EventFailedLeave{
480 Reason: "You are not currently in a match.",
481 })
482 continue
483 }
484
485 if cmd.client.playerNumber == 1 {
486 clientGame.rejoin1 = false
487 } else {
488 clientGame.rejoin2 = false
489 }
490
491 clientGame.removeClient(cmd.client)
492 case bgammon.CommandDouble, "d":
493 if clientGame == nil {
494 cmd.client.sendNotice("You are not currently in a match.")
495 continue
496 } else if clientGame.Winner != 0 {
497 continue
498 }
499
500 if clientGame.Turn != cmd.client.playerNumber {
501 cmd.client.sendNotice("It is not your turn.")
502 continue
503 }
504
505 gameState := &bgammon.GameState{
506 Game: clientGame.Game,
507 PlayerNumber: cmd.client.playerNumber,
508 Available: clientGame.LegalMoves(false),
509 }
510 if !gameState.MayDouble() {
511 cmd.client.sendNotice("You may not double at this time.")
512 continue
513 }
514
515 if clientGame.DoublePlayer != 0 && clientGame.DoublePlayer != cmd.client.playerNumber {
516 cmd.client.sendNotice("You do not currently hold the doubling cube.")
517 continue
518 }
519
520 opponent := clientGame.opponent(cmd.client)
521 if opponent == nil {
522 cmd.client.sendNotice("You may not double until your opponent rejoins the match.")
523 continue
524 }
525
526 clientGame.DoubleOffered = true
527 clientGame.NextPartialTurn(opponent.playerNumber)
528
529 cmd.client.sendNotice(fmt.Sprintf("Double offered to opponent (%d points).", clientGame.DoubleValue*2))
530 clientGame.opponent(cmd.client).sendNotice(fmt.Sprintf("%s offers a double (%d points).", cmd.client.name, clientGame.DoubleValue*2))
531
532 clientGame.eachClient(func(client *serverClient) {
533 if client.json {
534 clientGame.sendBoard(client, false)
535 }
536 })
537 case bgammon.CommandResign:
538 if clientGame == nil {
539 cmd.client.sendNotice("You are not currently in a match.")
540 continue
541 } else if clientGame.Winner != 0 {
542 continue
543 }
544
545 gameState := &bgammon.GameState{
546 Game: clientGame.Game,
547 PlayerNumber: cmd.client.playerNumber,
548 Available: clientGame.LegalMoves(false),
549 }
550 if !gameState.MayResign() {
551 cmd.client.sendNotice("You may not resign at this time.")
552 continue
553 }
554
555 opponent := clientGame.opponent(cmd.client)
556 if opponent == nil {
557 cmd.client.sendNotice("You may not resign until your opponent rejoins the match.")
558 continue
559 }
560
561 clientGame.NextPartialTurn(opponent.playerNumber)
562
563 cmd.client.sendNotice("Declined double offer")
564 clientGame.opponent(cmd.client).sendNotice(fmt.Sprintf("%s declined double offer.", cmd.client.name))
565
566 clientGame.replay = append([][]byte{[]byte(fmt.Sprintf("i %d %s %s %d %d %d %d %d %d", clientGame.Started.Unix(), clientGame.Player1.Name, clientGame.Player2.Name, clientGame.Points, clientGame.Player1.Points, clientGame.Player2.Points, clientGame.Winner, clientGame.DoubleValue, clientGame.Variant))}, clientGame.replay...)
567
568 clientGame.replay = append(clientGame.replay, []byte(fmt.Sprintf("%d d %d 0", clientGame.Turn, clientGame.DoubleValue*2)))
569
570 var reset bool
571 if cmd.client.playerNumber == 1 {
572 clientGame.Player2.Points = clientGame.Player2.Points + clientGame.DoubleValue
573 if clientGame.Player2.Points >= clientGame.Points {
574 clientGame.Winner = 2
575 clientGame.Ended = time.Now()
576 } else {
577 reset = true
578 }
579 } else {
580 clientGame.Player1.Points = clientGame.Player2.Points + clientGame.DoubleValue
581 if clientGame.Player1.Points >= clientGame.Points {
582 clientGame.Winner = 1
583 clientGame.Ended = time.Now()
584 } else {
585 reset = true
586 }
587 }
588
589 var winEvent *bgammon.EventWin
590 if clientGame.Winner != 0 {
591 winEvent = &bgammon.EventWin{
592 Points: clientGame.DoubleValue,
593 }
594 if clientGame.Winner == 1 {
595 winEvent.Player = clientGame.Player1.Name
596 } else {
597 winEvent.Player = clientGame.Player2.Name
598 }
599
600 err := recordGameResult(clientGame, 4, clientGame.replay)
601 if err != nil {
602 log.Fatalf("failed to record game result: %s", err)
603 }
604
605 if !reset {
606 err := recordMatchResult(clientGame, matchTypeCasual)
607 if err != nil {
608 log.Fatalf("failed to record match result: %s", err)
609 }
610 }
611 }
612
613 if reset {
614 clientGame.Reset()
615 clientGame.replay = clientGame.replay[:0]
616 }
617
618 clientGame.eachClient(func(client *serverClient) {
619 clientGame.sendBoard(client, false)
620 if winEvent != nil {
621 client.sendEvent(winEvent)
622 }
623 })
624 case bgammon.CommandRoll, "r":
625 if clientGame == nil {
626 cmd.client.sendEvent(&bgammon.EventFailedRoll{
627 Reason: "You are not currently in a match.",
628 })
629 continue
630 } else if clientGame.Winner != 0 {
631 continue
632 }
633
634 opponent := clientGame.opponent(cmd.client)
635 if opponent == nil {
636 cmd.client.sendEvent(&bgammon.EventFailedRoll{
637 Reason: "You may not roll until your opponent rejoins the match.",
638 })
639 continue
640 }
641
642 if !clientGame.roll(cmd.client.playerNumber) {
643 cmd.client.sendEvent(&bgammon.EventFailedRoll{
644 Reason: "It is not your turn to roll.",
645 })
646 continue
647 }
648
649 clientGame.eachClient(func(client *serverClient) {
650 ev := &bgammon.EventRolled{
651 Roll1: clientGame.Roll1,
652 Roll2: clientGame.Roll2,
653 Roll3: clientGame.Roll3,
654 }
655 ev.Player = string(cmd.client.name)
656 if clientGame.Turn == 0 && client.playerNumber == 2 {
657 ev.Roll1, ev.Roll2 = ev.Roll2, ev.Roll1
658 }
659 client.sendEvent(ev)
660 })
661
662
663 if clientGame.Turn == 0 && clientGame.Roll1 != 0 && clientGame.Roll2 != 0 {
664 reroll := func() {
665 clientGame.Roll1 = 0
666 clientGame.Roll2 = 0
667 if !clientGame.roll(clientGame.Turn) {
668 log.Fatal("failed to re-roll while starting game")
669 }
670
671 ev := &bgammon.EventRolled{
672 Roll1: clientGame.Roll1,
673 Roll2: clientGame.Roll2,
674 Roll3: clientGame.Roll3,
675 }
676 ev.Player = string(clientGame.Player1.Name)
677 if clientGame.Turn == 2 {
678 ev.Player = string(clientGame.Player2.Name)
679 }
680 clientGame.eachClient(func(client *serverClient) {
681 client.sendEvent(ev)
682 })
683 }
684
685 if clientGame.Roll1 > clientGame.Roll2 {
686 clientGame.Turn = 1
687 if clientGame.Variant != bgammon.VariantBackgammon {
688 reroll()
689 }
690 } else if clientGame.Roll2 > clientGame.Roll1 {
691 clientGame.Turn = 2
692 if clientGame.Variant != bgammon.VariantBackgammon {
693 reroll()
694 }
695 } else {
696 for {
697 clientGame.Roll1 = 0
698 clientGame.Roll2 = 0
699 if !clientGame.roll(1) {
700 log.Fatal("failed to re-roll to determine starting player")
701 }
702 if !clientGame.roll(2) {
703 log.Fatal("failed to re-roll to determine starting player")
704 }
705 clientGame.eachClient(func(client *serverClient) {
706 {
707 ev := &bgammon.EventRolled{
708 Roll1: clientGame.Roll1,
709 }
710 ev.Player = clientGame.Player1.Name
711 if clientGame.Turn == 0 && client.playerNumber == 2 {
712 ev.Roll1, ev.Roll2 = ev.Roll2, ev.Roll1
713 }
714 client.sendEvent(ev)
715 }
716 {
717 ev := &bgammon.EventRolled{
718 Roll1: clientGame.Roll1,
719 Roll2: clientGame.Roll2,
720 }
721 ev.Player = clientGame.Player2.Name
722 if clientGame.Turn == 0 && client.playerNumber == 2 {
723 ev.Roll1, ev.Roll2 = ev.Roll2, ev.Roll1
724 }
725 client.sendEvent(ev)
726 }
727 })
728 if clientGame.Roll1 > clientGame.Roll2 {
729 clientGame.Turn = 1
730 if clientGame.Variant != bgammon.VariantBackgammon {
731 reroll()
732 }
733 break
734 } else if clientGame.Roll2 > clientGame.Roll1 {
735 clientGame.Turn = 2
736 if clientGame.Variant != bgammon.VariantBackgammon {
737 reroll()
738 }
739 break
740 }
741 }
742 }
743 }
744
745 clientGame.NextPartialTurn(clientGame.Turn)
746
747 forcedMove := clientGame.playForcedMoves()
748 if forcedMove && len(clientGame.LegalMoves(false)) == 0 {
749 chooseRoll := clientGame.Variant == bgammon.VariantAceyDeucey && ((clientGame.Roll1 == 1 && clientGame.Roll2 == 2) || (clientGame.Roll1 == 2 && clientGame.Roll2 == 1)) && len(clientGame.Moves) == 2
750 if clientGame.Variant != bgammon.VariantAceyDeucey || !chooseRoll {
751 clientGame.recordEvent()
752 clientGame.nextTurn(false)
753 continue
754 }
755 }
756
757 clientGame.eachClient(func(client *serverClient) {
758 if clientGame.Turn != 0 || !client.json {
759 clientGame.sendBoard(client, false)
760 }
761 })
762 case bgammon.CommandMove, "m", "mv":
763 if clientGame == nil {
764 cmd.client.sendEvent(&bgammon.EventFailedMove{
765 Reason: "You are not currently in a match.",
766 })
767 continue
768 } else if clientGame.Winner != 0 {
769 clientGame.sendBoard(cmd.client, false)
770 continue
771 }
772
773 if clientGame.Turn != cmd.client.playerNumber {
774 cmd.client.sendEvent(&bgammon.EventFailedMove{
775 Reason: "It is not your turn to move.",
776 })
777 continue
778 }
779
780 opponent := clientGame.opponent(cmd.client)
781 if opponent == nil {
782 cmd.client.sendEvent(&bgammon.EventFailedMove{
783 Reason: "You may not move until your opponent rejoins the match.",
784 })
785 continue
786 }
787
788 sendUsage := func() {
789 cmd.client.sendEvent(&bgammon.EventFailedMove{
790 Reason: "Specify one or more moves in the form FROM/TO. For example: 8/4 6/4",
791 })
792 }
793
794 if len(params) == 0 {
795 sendUsage()
796 continue
797 }
798
799 var moves [][]int8
800 for i := range params {
801 split := bytes.Split(params[i], []byte("/"))
802 if len(split) != 2 {
803 sendUsage()
804 continue COMMANDS
805 }
806 from := bgammon.ParseSpace(string(split[0]))
807 if from == -1 {
808 sendUsage()
809 continue COMMANDS
810 }
811 to := bgammon.ParseSpace(string(split[1]))
812 if to == -1 {
813 sendUsage()
814 continue COMMANDS
815 }
816 if !bgammon.ValidSpace(from) || !bgammon.ValidSpace(to) {
817 cmd.client.sendEvent(&bgammon.EventFailedMove{
818 From: from,
819 To: to,
820 Reason: "Illegal move.",
821 })
822 continue COMMANDS
823 }
824
825 from, to = bgammon.FlipSpace(from, cmd.client.playerNumber, clientGame.Variant), bgammon.FlipSpace(to, cmd.client.playerNumber, clientGame.Variant)
826 moves = append(moves, []int8{from, to})
827 }
828
829 ok, expandedMoves := clientGame.AddMoves(moves, false)
830 if !ok {
831 cmd.client.sendEvent(&bgammon.EventFailedMove{
832 From: 0,
833 To: 0,
834 Reason: "Illegal move.",
835 })
836 continue
837 }
838
839 clientGame.eachClient(func(client *serverClient) {
840 ev := &bgammon.EventMoved{
841 Moves: bgammon.FlipMoves(expandedMoves, client.playerNumber, clientGame.Variant),
842 }
843 ev.Player = string(cmd.client.name)
844 client.sendEvent(ev)
845
846 clientGame.sendBoard(client, false)
847 })
848
849 clientGame.handleWin()
850 case bgammon.CommandReset:
851 if clientGame == nil {
852 cmd.client.sendNotice("You are not currently in a match.")
853 continue
854 } else if clientGame.Winner != 0 {
855 continue
856 }
857
858 if clientGame.Turn != cmd.client.playerNumber {
859 cmd.client.sendNotice("It is not your turn.")
860 continue
861 }
862
863 if len(clientGame.Moves) == 0 {
864 continue
865 }
866
867 l := len(clientGame.Moves)
868 undoMoves := make([][]int8, l)
869 for i, move := range clientGame.Moves {
870 undoMoves[l-1-i] = []int8{move[1], move[0]}
871 }
872 ok, _ := clientGame.AddMoves(undoMoves, false)
873 if !ok {
874 cmd.client.sendNotice("Failed to undo move: invalid move.")
875 } else {
876 clientGame.eachClient(func(client *serverClient) {
877 ev := &bgammon.EventMoved{
878 Moves: bgammon.FlipMoves(undoMoves, client.playerNumber, clientGame.Variant),
879 }
880 ev.Player = string(cmd.client.name)
881
882 client.sendEvent(ev)
883 clientGame.sendBoard(client, false)
884 })
885 }
886 case bgammon.CommandOk, "k":
887 if clientGame == nil {
888 cmd.client.sendNotice("You are not currently in a match.")
889 continue
890 } else if clientGame.Winner != 0 {
891 continue
892 }
893
894 opponent := clientGame.opponent(cmd.client)
895 if opponent == nil {
896 cmd.client.sendNotice("You must wait until your opponent rejoins the match before continuing the game.")
897 continue
898 }
899
900 if clientGame.DoubleOffered {
901 if clientGame.Turn != cmd.client.playerNumber {
902 opponent := clientGame.opponent(cmd.client)
903 if opponent == nil {
904 cmd.client.sendNotice("You may not accept the double until your opponent rejoins the match.")
905 continue
906 }
907
908 clientGame.DoubleOffered = false
909 clientGame.DoubleValue = clientGame.DoubleValue * 2
910 clientGame.DoublePlayer = cmd.client.playerNumber
911 clientGame.NextPartialTurn(opponent.playerNumber)
912
913 cmd.client.sendNotice("Accepted double.")
914 opponent.sendNotice(fmt.Sprintf("%s accepted double.", cmd.client.name))
915
916 clientGame.replay = append(clientGame.replay, []byte(fmt.Sprintf("%d d %d 1", clientGame.Turn, clientGame.DoubleValue)))
917 clientGame.eachClient(func(client *serverClient) {
918 clientGame.sendBoard(client, false)
919 })
920 } else {
921 cmd.client.sendNotice("Waiting for response from opponent.")
922 }
923 continue
924 } else if clientGame.Turn != cmd.client.playerNumber {
925 cmd.client.sendNotice("It is not your turn.")
926 continue
927 }
928
929 if clientGame.Roll1 == 0 || clientGame.Roll2 == 0 {
930 cmd.client.sendNotice("You must roll first.")
931 continue
932 }
933
934 legalMoves := clientGame.LegalMoves(false)
935 if len(legalMoves) != 0 {
936 available := bgammon.FlipMoves(legalMoves, cmd.client.playerNumber, clientGame.Variant)
937 bgammon.SortMoves(available)
938 cmd.client.sendEvent(&bgammon.EventFailedOk{
939 Reason: fmt.Sprintf("The following legal moves are available: %s", bgammon.FormatMoves(available)),
940 })
941 continue
942 }
943
944 if clientGame.Variant == bgammon.VariantAceyDeucey && ((clientGame.Roll1 == 1 && clientGame.Roll2 == 2) || (clientGame.Roll1 == 2 && clientGame.Roll2 == 1)) && len(clientGame.Moves) == 2 {
945 var doubles int
946 if len(params) > 0 {
947 doubles, _ = strconv.Atoi(string(params[0]))
948 }
949 if doubles < 1 || doubles > 6 {
950 cmd.client.sendEvent(&bgammon.EventFailedOk{
951 Reason: "Choose which doubles you want for your acey-deucey.",
952 })
953 continue
954 }
955
956 clientGame.recordEvent()
957 clientGame.nextTurn(true)
958 clientGame.Roll1, clientGame.Roll2 = int8(doubles), int8(doubles)
959 clientGame.Reroll = true
960
961 clientGame.eachClient(func(client *serverClient) {
962 ev := &bgammon.EventRolled{
963 Roll1: clientGame.Roll1,
964 Roll2: clientGame.Roll2,
965 Selected: true,
966 }
967 ev.Player = string(cmd.client.name)
968 client.sendEvent(ev)
969 clientGame.sendBoard(client, false)
970 })
971 } else if clientGame.Variant == bgammon.VariantAceyDeucey && clientGame.Reroll {
972 clientGame.recordEvent()
973 clientGame.nextTurn(true)
974 clientGame.Roll1, clientGame.Roll2 = 0, 0
975 if !clientGame.roll(cmd.client.playerNumber) {
976 cmd.client.Terminate("Server error")
977 opponent.Terminate("Server error")
978 continue
979 }
980 clientGame.Reroll = false
981
982 clientGame.eachClient(func(client *serverClient) {
983 ev := &bgammon.EventRolled{
984 Roll1: clientGame.Roll1,
985 Roll2: clientGame.Roll2,
986 }
987 ev.Player = string(cmd.client.name)
988 client.sendEvent(ev)
989 clientGame.sendBoard(client, false)
990 })
991 } else {
992 clientGame.recordEvent()
993 clientGame.nextTurn(false)
994 }
995 case bgammon.CommandRematch, "rm":
996 if clientGame == nil {
997 cmd.client.sendNotice("You are not currently in a match.")
998 continue
999 } else if clientGame.Winner == 0 {
1000 cmd.client.sendNotice("The match you are in is still in progress.")
1001 continue
1002 } else if clientGame.rematch == cmd.client.playerNumber {
1003 cmd.client.sendNotice("You have already requested a rematch.")
1004 continue
1005 } else if clientGame.client1 == nil || clientGame.client2 == nil {
1006 cmd.client.sendNotice("Your opponent left the match.")
1007 continue
1008 } else if clientGame.rematch != 0 && clientGame.rematch != cmd.client.playerNumber {
1009 s.gamesLock.Lock()
1010
1011 newGame := newServerGame(<-s.newGameIDs, clientGame.Variant)
1012 newGame.name = clientGame.name
1013 newGame.Points = clientGame.Points
1014 newGame.password = clientGame.password
1015 newGame.client1 = clientGame.client1
1016 newGame.client2 = clientGame.client2
1017 newGame.spectators = make([]*serverClient, len(clientGame.spectators))
1018 copy(newGame.spectators, clientGame.spectators)
1019 newGame.Player1.Name = clientGame.Player1.Name
1020 newGame.Player2.Name = clientGame.Player2.Name
1021 newGame.Player1.Points = clientGame.Player1.Points
1022 newGame.Player2.Points = clientGame.Player2.Points
1023 newGame.allowed1 = clientGame.allowed1
1024 newGame.allowed2 = clientGame.allowed2
1025 s.games = append(s.games, newGame)
1026
1027 clientGame.client1 = nil
1028 clientGame.client2 = nil
1029 clientGame.spectators = nil
1030
1031 s.gamesLock.Unlock()
1032
1033 {
1034 ev1 := &bgammon.EventJoined{
1035 GameID: newGame.id,
1036 PlayerNumber: 1,
1037 }
1038 ev1.Player = newGame.Player1.Name
1039 ev2 := &bgammon.EventJoined{
1040 GameID: newGame.id,
1041 PlayerNumber: 2,
1042 }
1043 ev2.Player = newGame.Player2.Name
1044 newGame.client1.sendEvent(ev1)
1045 newGame.client1.sendEvent(ev2)
1046 newGame.sendBoard(newGame.client1, false)
1047 }
1048
1049 {
1050 ev1 := &bgammon.EventJoined{
1051 GameID: newGame.id,
1052 PlayerNumber: 1,
1053 }
1054 ev1.Player = newGame.Player2.Name
1055 ev2 := &bgammon.EventJoined{
1056 GameID: newGame.id,
1057 PlayerNumber: 2,
1058 }
1059 ev2.Player = newGame.Player1.Name
1060 newGame.client2.sendEvent(ev1)
1061 newGame.client2.sendEvent(ev2)
1062 newGame.sendBoard(newGame.client2, false)
1063 }
1064
1065 for _, spectator := range newGame.spectators {
1066 newGame.sendBoard(spectator, false)
1067 }
1068 } else {
1069 clientGame.rematch = cmd.client.playerNumber
1070
1071 clientGame.opponent(cmd.client).sendNotice("Your opponent would like to play again. Type /rematch to accept.")
1072 cmd.client.sendNotice("Rematch offer sent.")
1073 continue
1074 }
1075 case bgammon.CommandBoard, "b":
1076 if clientGame == nil {
1077 cmd.client.sendNotice("You are not currently in a match.")
1078 continue
1079 }
1080
1081 clientGame.sendBoard(cmd.client, false)
1082 case bgammon.CommandPassword:
1083 if cmd.client.account == nil {
1084 cmd.client.sendNotice("Failed to change password: you are logged in as a guest.")
1085 continue
1086 } else if len(params) < 2 {
1087 cmd.client.sendNotice("Please specify your old and new passwords as follows: password <old> <new>")
1088 continue
1089 }
1090
1091 a, err := loginAccount(s.passwordSalt, cmd.client.name, params[0])
1092 if err != nil || a == nil || a.id == 0 {
1093 cmd.client.sendNotice("Failed to change password: incorrect existing password.")
1094 continue
1095 }
1096
1097 err = setAccountPassword(s.passwordSalt, a.id, string(bytes.Join(params[1:], []byte("_"))))
1098 if err != nil {
1099 cmd.client.sendNotice("Failed to change password.")
1100 continue
1101 }
1102 cmd.client.sendNotice("Password changed successfully.")
1103 case bgammon.CommandSet:
1104 if len(params) < 2 {
1105 cmd.client.sendNotice("Please specify the setting name and value as follows: set <name> <value>")
1106 continue
1107 }
1108
1109 name := string(bytes.ToLower(params[0]))
1110 settings := []string{"autoplay", "highlight", "pips", "moves", "flip", "advanced", "speed"}
1111 var found bool
1112 for i := range settings {
1113 if name == settings[i] {
1114 found = true
1115 break
1116 }
1117 }
1118 if !found {
1119 cmd.client.sendNotice("Please specify the setting name and value as follows: set <name> <value>")
1120 continue
1121 }
1122
1123 value, err := strconv.Atoi(string(params[1]))
1124 if err != nil || value < 0 || (name == "speed" && value > 3) || (name != "speed" && value > 1) {
1125 cmd.client.sendNotice("Invalid setting value provided.")
1126 continue
1127 }
1128
1129 if name == "autoplay" {
1130 cmd.client.autoplay = value == 1
1131 }
1132
1133 if cmd.client.account == nil {
1134 continue
1135 }
1136 _ = setAccountSetting(cmd.client.account.id, name, value)
1137 case bgammon.CommandReplay:
1138 var (
1139 id int
1140 replay []byte
1141 err error
1142 )
1143 if len(params) == 0 {
1144 if clientGame == nil || clientGame.Winner == 0 {
1145 cmd.client.sendNotice("Please specify the game as follows: replay <id>")
1146 continue
1147 }
1148 id = -1
1149 replay = bytes.Join(clientGame.replay, []byte("\n"))
1150 } else {
1151 id, err = strconv.Atoi(string(params[0]))
1152 if err != nil || id < 0 {
1153 cmd.client.sendNotice("Invalid replay ID provided.")
1154 continue
1155 }
1156 replay, err = replayByID(id)
1157 if err != nil {
1158 cmd.client.sendNotice("Invalid replay ID provided.")
1159 continue
1160 }
1161 }
1162 if len(replay) == 0 {
1163 cmd.client.sendNotice("No replay was recorded for that game.")
1164 continue
1165 }
1166 cmd.client.sendEvent(&bgammon.EventReplay{
1167 ID: id,
1168 Content: replay,
1169 })
1170 case bgammon.CommandHistory:
1171 if len(params) == 0 {
1172 cmd.client.sendNotice("Please specify the player as follows: history <username>")
1173 continue
1174 }
1175 const historyPageSize = 50
1176
1177 page := 1
1178 if len(params) > 1 {
1179 p, err := strconv.Atoi(string(params[1]))
1180 if err == nil && p >= 1 {
1181 page = p
1182 }
1183 }
1184
1185 matches, err := matchHistory(string(params[0]))
1186 if err != nil {
1187 cmd.client.sendNotice("Invalid replay ID provided.")
1188 continue
1189 }
1190
1191 pages := (len(matches) / historyPageSize)
1192 if pages == 0 {
1193 pages = 1
1194 }
1195
1196 ev := &bgammon.EventHistory{
1197 Page: page,
1198 Pages: pages,
1199 }
1200 if len(matches) > 0 && page <= pages {
1201 max := page * historyPageSize
1202 if max > len(matches) {
1203 max = len(matches)
1204 }
1205 ev.Matches = matches[(page-1)*historyPageSize : max]
1206 }
1207
1208 ev.Player = string(params[0])
1209 a, err := accountByUsername(string(params[0]))
1210 if err == nil && a != nil {
1211 ev.CasualBackgammonSingle = a.casual.backgammonSingle / 100
1212 ev.CasualBackgammonMulti = a.casual.backgammonMulti / 100
1213 ev.CasualAceyDeuceySingle = a.casual.aceySingle / 100
1214 ev.CasualAceyDeuceyMulti = a.casual.aceyMulti / 100
1215 ev.CasualTabulaSingle = a.casual.tabulaSingle / 100
1216 ev.CasualTabulaMulti = a.casual.tabulaMulti / 100
1217 }
1218 cmd.client.sendEvent(ev)
1219 case bgammon.CommandDisconnect:
1220 if clientGame != nil {
1221 clientGame.removeClient(cmd.client)
1222 }
1223 cmd.client.Terminate("Client disconnected")
1224 case bgammon.CommandPong:
1225
1226 case "endgame":
1227 if !allowDebugCommands {
1228 cmd.client.sendNotice("You are not allowed to use that command.")
1229 continue
1230 }
1231
1232 if clientGame == nil {
1233 cmd.client.sendNotice("You are not currently in a match.")
1234 continue
1235 }
1236
1237 clientGame.Turn = 1
1238 clientGame.Roll1 = 6
1239 clientGame.Roll2 = 1
1240 clientGame.Roll3 = 1
1241 clientGame.Variant = 2
1242 clientGame.Player1.Entered = true
1243 clientGame.Player2.Entered = true
1244 clientGame.Board = []int8{0, 0, 0, 0, 0, -3, 0, 0, -3, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 9, 3, 1, -5, 1, 1, 0}
1245
1246 log.Println(clientGame.Board[0:28])
1247
1248 clientGame.eachClient(func(client *serverClient) {
1249 clientGame.sendBoard(client, false)
1250 })
1251 default:
1252 log.Printf("Received unknown command from client %s: %s", cmd.client.label(), cmd.command)
1253 cmd.client.sendNotice(fmt.Sprintf("Unknown command: %s", cmd.command))
1254 }
1255 }
1256 }
1257
View as plain text