...

Source file src/code.rocket9labs.com/tslocum/bgammon/pkg/server/server.go

Documentation: code.rocket9labs.com/tslocum/bgammon/pkg/server

     1  package server
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"crypto/rand"
     7  	"encoding/base64"
     8  	"fmt"
     9  	"log"
    10  	"math/big"
    11  	"net"
    12  	"net/http"
    13  	"os"
    14  	"os/exec"
    15  	"regexp"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"time"
    20  
    21  	"code.rocket9labs.com/tslocum/bgammon"
    22  )
    23  
    24  const clientTimeout = 40 * time.Second
    25  
    26  const inactiveLimit = 600 // 10 minutes.
    27  
    28  var allowDebugCommands bool
    29  
    30  var (
    31  	onlyNumbers            = regexp.MustCompile(`^[0-9]+$`)
    32  	guestName              = regexp.MustCompile(`^guest[0-9]+$`)
    33  	alphaNumericUnderscore = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
    34  )
    35  
    36  type serverCommand struct {
    37  	client  *serverClient
    38  	command []byte
    39  }
    40  
    41  type server struct {
    42  	clients      []*serverClient
    43  	games        []*serverGame
    44  	listeners    []net.Listener
    45  	newGameIDs   chan int
    46  	newClientIDs chan int
    47  	commands     chan serverCommand
    48  	welcome      []byte
    49  
    50  	gamesLock   sync.RWMutex
    51  	clientsLock sync.Mutex
    52  
    53  	gamesCache     []byte
    54  	gamesCacheTime time.Time
    55  	gamesCacheLock sync.Mutex
    56  
    57  	statsCache     [4][]byte
    58  	statsCacheTime time.Time
    59  	statsCacheLock sync.Mutex
    60  
    61  	leaderboardCache     [12][]byte
    62  	leaderboardCacheTime time.Time
    63  	leaderboardCacheLock sync.Mutex
    64  
    65  	mailServer   string
    66  	passwordSalt string
    67  	resetSalt    string
    68  
    69  	tz *time.Location
    70  
    71  	relayChat bool // Chats are not relayed normally. This option is only used by local servers.
    72  	verbose   bool
    73  }
    74  
    75  func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, relayChat bool, verbose bool, allowDebug bool) *server {
    76  	const bufferSize = 10
    77  	s := &server{
    78  		newGameIDs:   make(chan int),
    79  		newClientIDs: make(chan int),
    80  		commands:     make(chan serverCommand, bufferSize),
    81  		welcome:      []byte("hello Welcome to bgammon.org! Please log in by sending the 'login' command. You may specify a username, otherwise you will be assigned a random username. If you specify a username, you may also specify a password. Have fun!"),
    82  		mailServer:   mailServer,
    83  		passwordSalt: passwordSalt,
    84  		resetSalt:    resetSalt,
    85  		relayChat:    relayChat,
    86  		verbose:      verbose,
    87  	}
    88  
    89  	if tz != "" {
    90  		var err error
    91  		s.tz, err = time.LoadLocation(tz)
    92  		if err != nil {
    93  			log.Fatalf("failed to parse timezone %s: %s", tz, err)
    94  		}
    95  	} else {
    96  		s.tz = time.UTC
    97  	}
    98  
    99  	if dataSource != "" {
   100  		err := connectDB(dataSource)
   101  		if err != nil {
   102  			log.Fatalf("failed to connect to database: %s", err)
   103  		}
   104  
   105  		err = testDBConnection()
   106  		if err != nil {
   107  			log.Fatalf("failed to test database connection: %s", err)
   108  		}
   109  
   110  		initDB()
   111  
   112  		log.Println("Connected to database successfully")
   113  	}
   114  
   115  	allowDebugCommands = allowDebug
   116  
   117  	/*gm := bgammon.NewGame(bgammon.VariantBackgammon)
   118  	gm.Turn = 1
   119  	gm.Roll1 = 2
   120  	gm.Roll2 = 3
   121  	log.Println(gm.MayBearOff(1, false))
   122  	gm.Player1.Entered = true
   123  	gm.Player2.Entered = true
   124  	log.Println(gm.Board)
   125  	//ok, expanded := gm.AddMoves([][]int8{{3, 1}}, false)
   126  	//log.Println(ok, expanded, "!")
   127  	log.Println(gm.MayBearOff(1, false))
   128  	gs := &bgammon.GameState{
   129  		Game:         gm,
   130  		PlayerNumber: 1,
   131  		Available:    gm.LegalMoves(false),
   132  	}
   133  	log.Printf("%+v", gs)
   134  	os.Exit(0)*/
   135  
   136  	go s.handleNewGameIDs()
   137  	go s.handleNewClientIDs()
   138  	go s.handleCommands()
   139  	go s.handleGames()
   140  	return s
   141  }
   142  
   143  func (s *server) ListenLocal() chan net.Conn {
   144  	conns := make(chan net.Conn)
   145  	go s.handleLocal(conns)
   146  	return conns
   147  }
   148  
   149  func (s *server) handleLocal(conns chan net.Conn) {
   150  	for {
   151  		local, remote := net.Pipe()
   152  
   153  		conns <- local
   154  		go s.handleConnection(remote)
   155  	}
   156  }
   157  
   158  func (s *server) Listen(network string, address string) {
   159  	if strings.ToLower(network) == "ws" {
   160  		go s.listenWebSocket(address)
   161  		return
   162  	}
   163  
   164  	log.Printf("Listening for %s connections on %s...", strings.ToUpper(network), address)
   165  	listener, err := net.Listen(network, address)
   166  	if err != nil {
   167  		log.Fatalf("failed to listen on %s: %s", address, err)
   168  	}
   169  	go s.handleListener(listener)
   170  	s.listeners = append(s.listeners, listener)
   171  }
   172  
   173  func (s *server) handleListener(listener net.Listener) {
   174  	for {
   175  		conn, err := listener.Accept()
   176  		if err != nil {
   177  			log.Fatalf("failed to accept connection: %s", err)
   178  		}
   179  		go s.handleConnection(conn)
   180  	}
   181  }
   182  
   183  func (s *server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
   184  	const bufferSize = 8
   185  	commands := make(chan []byte, bufferSize)
   186  	events := make(chan []byte, bufferSize)
   187  
   188  	wsClient := newWebSocketClient(r, w, commands, events, s.verbose)
   189  	if wsClient == nil {
   190  		return
   191  	}
   192  
   193  	now := time.Now().Unix()
   194  
   195  	c := &serverClient{
   196  		id:        <-s.newClientIDs,
   197  		accountID: -1,
   198  		connected: now,
   199  		active:    now,
   200  		commands:  commands,
   201  		Client:    wsClient,
   202  	}
   203  	s.handleClient(c)
   204  }
   205  
   206  func (s *server) nameAllowed(username []byte) bool {
   207  	return !guestName.Match(username)
   208  }
   209  
   210  func (s *server) clientByUsername(username []byte) *serverClient {
   211  	lower := bytes.ToLower(username)
   212  	for _, c := range s.clients {
   213  		if bytes.Equal(bytes.ToLower(c.name), lower) {
   214  			return c
   215  		}
   216  	}
   217  	return nil
   218  }
   219  
   220  func (s *server) addClient(c *serverClient) {
   221  	s.clientsLock.Lock()
   222  	defer s.clientsLock.Unlock()
   223  
   224  	s.clients = append(s.clients, c)
   225  }
   226  
   227  func (s *server) removeClient(c *serverClient) {
   228  	g := s.gameByClient(c)
   229  	if g != nil {
   230  		g.removeClient(c)
   231  	}
   232  	c.Terminate("")
   233  
   234  	close(c.commands)
   235  
   236  	s.clientsLock.Lock()
   237  	defer s.clientsLock.Unlock()
   238  
   239  	for i, sc := range s.clients {
   240  		if sc == c {
   241  			s.clients = append(s.clients[:i], s.clients[i+1:]...)
   242  			return
   243  		}
   244  	}
   245  }
   246  
   247  func (s *server) handleGames() {
   248  	t := time.NewTicker(time.Minute)
   249  	for range t.C {
   250  		s.gamesLock.Lock()
   251  
   252  		i := 0
   253  		for _, g := range s.games {
   254  			if !g.PartialHandled() && g.Player1.Rating != 0 && g.Player2.Rating != 0 {
   255  				partialTurn := g.PartialTurn()
   256  				if partialTurn != 0 {
   257  					total := g.PartialTime()
   258  					switch partialTurn {
   259  					case 1:
   260  						total += g.Player1.Inactive
   261  					case 2:
   262  						total += g.Player2.Inactive
   263  					}
   264  					if total >= inactiveLimit {
   265  						g.inactive = partialTurn
   266  						g.SetPartialHandled(true)
   267  						if !g.terminated() {
   268  							var player *serverClient
   269  							var opponent *serverClient
   270  							switch partialTurn {
   271  							case 1:
   272  								player = g.client1
   273  								opponent = g.client2
   274  							case 2:
   275  								player = g.client2
   276  								opponent = g.client1
   277  							}
   278  							if player != nil {
   279  								player.sendNotice("You have been inactive for more than ten minutes. If your opponent leaves the match they will receive a win.")
   280  							}
   281  							if opponent != nil {
   282  								opponent.sendNotice("Your opponent has been inactive for more than ten minutes. You may continue playing or leave the match at any time and receive a win.")
   283  							}
   284  						}
   285  					}
   286  				}
   287  			}
   288  
   289  			if !g.terminated() {
   290  				s.games[i] = g
   291  				i++
   292  			} else if g.Winner == 0 && (g.inactive != 0 || g.forefeit != 0) {
   293  				if g.inactive != 0 {
   294  					g.Winner = 1
   295  					if g.inactive == 1 {
   296  						g.Winner = 2
   297  					}
   298  				} else {
   299  					g.Winner = 1
   300  					if g.forefeit == 1 {
   301  						g.Winner = 2
   302  					}
   303  				}
   304  				err := recordMatchResult(g, matchTypeCasual)
   305  				if err != nil {
   306  					log.Fatalf("failed to record match result: %s", err)
   307  				}
   308  			}
   309  		}
   310  		for j := i; j < len(s.games); j++ {
   311  			s.games[j] = nil // Allow memory to be deallocated.
   312  		}
   313  		s.games = s.games[:i]
   314  
   315  		s.gamesLock.Unlock()
   316  	}
   317  }
   318  
   319  func (s *server) handleClient(c *serverClient) {
   320  	s.addClient(c)
   321  
   322  	log.Printf("Client %s connected", c.label())
   323  
   324  	go s.handlePingClient(c)
   325  	go s.handleClientCommands(c)
   326  
   327  	c.HandleReadWrite()
   328  
   329  	// Remove client.
   330  	s.removeClient(c)
   331  
   332  	log.Printf("Client %s disconnected", c.label())
   333  }
   334  
   335  func (s *server) handleConnection(conn net.Conn) {
   336  	const bufferSize = 8
   337  	commands := make(chan []byte, bufferSize)
   338  	events := make(chan []byte, bufferSize)
   339  
   340  	now := time.Now().Unix()
   341  
   342  	c := &serverClient{
   343  		id:        <-s.newClientIDs,
   344  		accountID: -1,
   345  		connected: now,
   346  		active:    now,
   347  		commands:  commands,
   348  		Client:    newSocketClient(conn, commands, events, s.verbose),
   349  	}
   350  	s.sendHello(c)
   351  	s.handleClient(c)
   352  }
   353  
   354  func (s *server) handlePingClient(c *serverClient) {
   355  	// TODO only ping when there is no recent activity
   356  	t := time.NewTicker(30 * time.Second)
   357  	for {
   358  		<-t.C
   359  
   360  		if c.Terminated() {
   361  			t.Stop()
   362  			return
   363  		}
   364  
   365  		if len(c.name) == 0 {
   366  			c.Terminate("User did not send login command within 30 seconds.")
   367  			t.Stop()
   368  			return
   369  		}
   370  
   371  		c.lastPing = time.Now().Unix()
   372  		c.sendEvent(&bgammon.EventPing{
   373  			Message: fmt.Sprintf("%d", c.lastPing),
   374  		})
   375  	}
   376  }
   377  
   378  func (s *server) handleClientCommands(c *serverClient) {
   379  	var command []byte
   380  	for command = range c.commands {
   381  		s.commands <- serverCommand{
   382  			client:  c,
   383  			command: command,
   384  		}
   385  	}
   386  }
   387  
   388  func (s *server) handleNewGameIDs() {
   389  	gameID := 1
   390  	for {
   391  		s.newGameIDs <- gameID
   392  		gameID++
   393  	}
   394  }
   395  
   396  func (s *server) handleNewClientIDs() {
   397  	clientID := 1
   398  	for {
   399  		s.newClientIDs <- clientID
   400  		clientID++
   401  	}
   402  }
   403  
   404  // randomUsername returns a random guest username, and assumes clients are already locked.
   405  func (s *server) randomUsername() []byte {
   406  	for {
   407  		name := []byte(fmt.Sprintf("Guest_%d", 100+RandInt(900)))
   408  
   409  		if s.clientByUsername(name) == nil {
   410  			return name
   411  		}
   412  	}
   413  }
   414  
   415  func (s *server) sendHello(c *serverClient) {
   416  	if c.json {
   417  		return
   418  	}
   419  	c.Write(s.welcome)
   420  }
   421  
   422  func (s *server) gameByClient(c *serverClient) *serverGame {
   423  	s.gamesLock.RLock()
   424  	defer s.gamesLock.RUnlock()
   425  
   426  	for _, g := range s.games {
   427  		if g.client1 == c || g.client2 == c {
   428  			return g
   429  		}
   430  		for _, spec := range g.spectators {
   431  			if spec == c {
   432  				return g
   433  			}
   434  		}
   435  	}
   436  	return nil
   437  }
   438  
   439  // Analyze returns match analysis information calculated by gnubg.
   440  func (s *server) Analyze(g *bgammon.Game) {
   441  	cmd := exec.Command("gnubg", "--tty")
   442  	stdin, err := cmd.StdinPipe()
   443  	if err != nil {
   444  		log.Fatal(err)
   445  	}
   446  	stdout, err := cmd.StdoutPipe()
   447  	if err != nil {
   448  		log.Fatal(err)
   449  	}
   450  	stderr, err := cmd.StderrPipe()
   451  	if err != nil {
   452  		log.Fatal(err)
   453  	}
   454  	err = cmd.Start()
   455  	if err != nil {
   456  		log.Fatal(err)
   457  	}
   458  
   459  	go func() {
   460  		scanner := bufio.NewScanner(stdout)
   461  		for scanner.Scan() {
   462  			log.Println("STDOUT", string(scanner.Bytes()))
   463  		}
   464  	}()
   465  
   466  	go func() {
   467  		scanner := bufio.NewScanner(stderr)
   468  		for scanner.Scan() {
   469  			log.Println("STDERR", string(scanner.Bytes()))
   470  		}
   471  	}()
   472  
   473  	stdin.Write([]byte(fmt.Sprintf("new game\nset board %s\nanalyze game\n", gnubgPosition(g))))
   474  
   475  	time.Sleep(2 * time.Second)
   476  	os.Exit(0)
   477  }
   478  
   479  func RandInt(max int) int {
   480  	i, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
   481  	if err != nil {
   482  		panic(err)
   483  	}
   484  	return int(i.Int64())
   485  }
   486  
   487  func gnubgPosition(g *bgammon.Game) string {
   488  	var opponent int8 = 2
   489  	start := 0
   490  	end := 25
   491  	boardStart := 1
   492  	boardEnd := 24
   493  	delta := 1
   494  	playerBarSpace := bgammon.SpaceBarPlayer
   495  	opponentBarSpace := bgammon.SpaceBarOpponent
   496  	switch g.Turn {
   497  	case 1:
   498  	case 2:
   499  		opponent = 1
   500  		start = 25
   501  		end = 0
   502  		boardStart = 24
   503  		boardEnd = 1
   504  		delta = -1
   505  		playerBarSpace = bgammon.SpaceBarOpponent
   506  		opponentBarSpace = bgammon.SpaceBarPlayer
   507  	default:
   508  		log.Fatalf("failed to analyze game: zero turn")
   509  	}
   510  
   511  	var buf []byte
   512  	for space := boardStart; space != end; space += delta {
   513  		playerCheckers := bgammon.PlayerCheckers(g.Board[space], g.Turn)
   514  		for i := int8(0); i < playerCheckers; i++ {
   515  			buf = append(buf, '1')
   516  		}
   517  		buf = append(buf, '0')
   518  	}
   519  	playerCheckers := bgammon.PlayerCheckers(g.Board[playerBarSpace], g.Turn)
   520  	for i := int8(0); i < playerCheckers; i++ {
   521  		buf = append(buf, '1')
   522  	}
   523  	buf = append(buf, '0')
   524  
   525  	for space := boardEnd; space != start; space -= delta {
   526  		opponentCheckers := bgammon.PlayerCheckers(g.Board[space], opponent)
   527  		for i := int8(0); i < opponentCheckers; i++ {
   528  			buf = append(buf, '1')
   529  		}
   530  		buf = append(buf, '0')
   531  	}
   532  	opponentCheckers := bgammon.PlayerCheckers(g.Board[opponentBarSpace], opponent)
   533  	for i := int8(0); i < opponentCheckers; i++ {
   534  		buf = append(buf, '1')
   535  	}
   536  	buf = append(buf, '0')
   537  
   538  	for i := len(buf); i < 80; i++ {
   539  		buf = append(buf, '0')
   540  	}
   541  
   542  	var out []byte
   543  	for i := 0; i < len(buf); i += 8 {
   544  		s := reverseString(string(buf[i : i+8]))
   545  		v, err := strconv.ParseUint(s, 2, 8)
   546  		if err != nil {
   547  			panic(err)
   548  		}
   549  		out = append(out, byte(v))
   550  	}
   551  
   552  	position := base64.StdEncoding.EncodeToString(out)
   553  	if len(position) == 0 {
   554  		return ""
   555  	}
   556  	for position[len(position)-1] == '=' {
   557  		position = position[:len(position)-1]
   558  	}
   559  	return position
   560  }
   561  
   562  func reverseString(s string) string {
   563  	runes := []rune(s)
   564  	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
   565  		runes[i], runes[j] = runes[j], runes[i]
   566  	}
   567  	return string(runes)
   568  }
   569  
   570  type ratingPlayer struct {
   571  	r       float64
   572  	rd      float64
   573  	sigma   float64
   574  	outcome float64
   575  }
   576  
   577  func (p ratingPlayer) R() float64 {
   578  	return p.r
   579  }
   580  
   581  func (p ratingPlayer) RD() float64 {
   582  	return p.rd
   583  }
   584  
   585  func (p ratingPlayer) Sigma() float64 {
   586  	return p.sigma
   587  }
   588  
   589  func (p ratingPlayer) SJ() float64 {
   590  	return p.outcome
   591  }
   592  

View as plain text