Kick idle players
This commit is contained in:
parent
40374caa22
commit
1508f25631
15 changed files with 827 additions and 816 deletions
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
0.1.1:
|
||||
- Kick inactive players
|
||||
|
||||
0.1.0:
|
||||
- Initial release
|
|
@ -10,6 +10,7 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~tslocum/netris/pkg/event"
|
||||
"git.sr.ht/~tslocum/netris/pkg/game"
|
||||
"git.sr.ht/~tslocum/netris/pkg/game/ssh"
|
||||
)
|
||||
|
@ -27,10 +28,6 @@ var (
|
|||
done = make(chan bool)
|
||||
)
|
||||
|
||||
const (
|
||||
LogTimeFormat = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
|
||||
|
@ -68,7 +65,7 @@ func main() {
|
|||
logger := make(chan string, game.LogQueueSize)
|
||||
go func() {
|
||||
for msg := range logger {
|
||||
log.Println(time.Now().Format(LogTimeFormat) + " " + msg)
|
||||
log.Println(time.Now().Format(event.LogFormat) + " " + msg)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
@ -449,6 +449,10 @@ func drawAll() {
|
|||
renderMultiplayerMatrix()
|
||||
}
|
||||
|
||||
func drawMessages() {
|
||||
recent.ScrollToEnd()
|
||||
}
|
||||
|
||||
func drawPlayerMatrix() {
|
||||
renderPlayerMatrix()
|
||||
renderPreviewMatrix()
|
||||
|
@ -462,6 +466,8 @@ func handleDraw() {
|
|||
var o event.DrawObject
|
||||
for o = range draw {
|
||||
switch o {
|
||||
case event.DrawMessages:
|
||||
app.QueueUpdateDraw(drawMessages)
|
||||
case event.DrawPlayerMatrix:
|
||||
app.QueueUpdateDraw(drawPlayerMatrix)
|
||||
case event.DrawMultiplayerMatrixes:
|
||||
|
@ -827,13 +833,9 @@ func logMessage(message string) {
|
|||
prefix = "\n"
|
||||
}
|
||||
|
||||
recent.Write([]byte(prefix + time.Now().Format(LogTimeFormat) + " " + message))
|
||||
recent.Write([]byte(prefix + time.Now().Format(event.LogFormat) + " " + message))
|
||||
|
||||
if prefix == "" {
|
||||
// Fix for small windows not auto-scrolling
|
||||
|
||||
recent.ScrollToEnd()
|
||||
}
|
||||
draw <- event.DrawMessages
|
||||
|
||||
logMutex.Unlock()
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@ import (
|
|||
"runtime/pprof"
|
||||
"strings"
|
||||
|
||||
"github.com/tslocum/tview"
|
||||
|
||||
"git.sr.ht/~tslocum/netris/pkg/event"
|
||||
"git.sr.ht/~tslocum/netris/pkg/game"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/tslocum/tview"
|
||||
)
|
||||
|
||||
type Keybinding struct {
|
||||
|
@ -57,7 +57,7 @@ func scrollMessages(direction int) {
|
|||
}
|
||||
recent.ScrollTo(r, 0)
|
||||
|
||||
draw <- event.DrawMessages
|
||||
draw <- event.DrawAll
|
||||
}
|
||||
|
||||
func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
|
||||
|
@ -298,10 +298,21 @@ func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
|
|||
|
||||
logMessage("Stopped profiling CPU usage")
|
||||
}
|
||||
} else if strings.HasPrefix(msg, "/version") {
|
||||
v := game.Version
|
||||
if v == "" {
|
||||
v = "unknown"
|
||||
}
|
||||
|
||||
logMessage(fmt.Sprintf("netris version %s", v))
|
||||
} else if strings.HasPrefix(msg, "/ping") {
|
||||
if activeGame != nil {
|
||||
activeGame.ProcessAction(event.ActionPing)
|
||||
}
|
||||
} else if strings.HasPrefix(msg, "/stats") {
|
||||
if activeGame != nil {
|
||||
activeGame.ProcessAction(event.ActionStats)
|
||||
}
|
||||
} else {
|
||||
if activeGame != nil {
|
||||
activeGame.Event <- &event.MessageEvent{Message: msg}
|
||||
|
|
|
@ -46,10 +46,6 @@ var (
|
|||
showLogLines = 7
|
||||
)
|
||||
|
||||
const (
|
||||
LogTimeFormat = "3:04:05"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
@ -185,7 +181,6 @@ func main() {
|
|||
|
||||
if connectNetwork != "unix" {
|
||||
logMessage(fmt.Sprintf("* Connecting to %s...", connectAddress))
|
||||
draw <- event.DrawMessages
|
||||
}
|
||||
|
||||
s := game.Connect(connectAddress)
|
||||
|
|
|
@ -7,8 +7,8 @@ builds:
|
|||
main: ./cmd/netris
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
# ldflags:
|
||||
# - -s -w -X git.sr.ht/~tslocum/netris/pkg/netris.Version={{.Version}}
|
||||
ldflags:
|
||||
- -s -w -X git.sr.ht/~tslocum/netris/pkg/game.Version={{.Version}}
|
||||
goos:
|
||||
- darwin
|
||||
- freebsd
|
||||
|
@ -21,8 +21,8 @@ builds:
|
|||
id: netris-server
|
||||
binary: netris-server
|
||||
main: ./cmd/netris-server
|
||||
# ldflags:
|
||||
# - -s -w -X git.sr.ht/~tslocum/netris/pkg/netris.Version={{.Version}}
|
||||
ldflags:
|
||||
- -s -w -X git.sr.ht/~tslocum/netris/pkg/game.Version={{.Version}}
|
||||
goos:
|
||||
- darwin
|
||||
- freebsd
|
||||
|
@ -38,7 +38,9 @@ archive:
|
|||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- CHANGELOG.md
|
||||
- CONFIGURATION.md
|
||||
- GAMEPLAY.md
|
||||
- LICENSE
|
||||
- README.md
|
||||
checksum:
|
||||
|
|
|
@ -11,4 +11,5 @@ const (
|
|||
ActionSoftDrop
|
||||
ActionHardDrop
|
||||
ActionPing
|
||||
ActionStats
|
||||
)
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package event
|
||||
|
||||
const (
|
||||
LogFormat = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Player int
|
||||
Message string
|
||||
|
|
231
pkg/game/command.go
Normal file
231
pkg/game/command.go
Normal file
|
@ -0,0 +1,231 @@
|
|||
package game
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~tslocum/netris/pkg/mino"
|
||||
)
|
||||
|
||||
type Command int
|
||||
|
||||
// The order of these constants must be preserved
|
||||
const (
|
||||
CommandUnknown Command = iota
|
||||
CommandDisconnect
|
||||
CommandPing
|
||||
CommandPong
|
||||
CommandNickname
|
||||
CommandMessage
|
||||
CommandNewGame
|
||||
CommandJoinGame
|
||||
CommandQuitGame
|
||||
CommandUpdateGame
|
||||
CommandStartGame
|
||||
CommandGameOver
|
||||
CommandUpdateMatrix
|
||||
CommandSendGarbage
|
||||
CommandReceiveGarbage
|
||||
CommandStats
|
||||
)
|
||||
|
||||
func (c Command) String() string {
|
||||
switch c {
|
||||
case CommandUnknown:
|
||||
return "Unknown"
|
||||
case CommandDisconnect:
|
||||
return "Disconnect"
|
||||
case CommandPing:
|
||||
return "Ping"
|
||||
case CommandPong:
|
||||
return "Pong"
|
||||
case CommandNickname:
|
||||
return "Nickname"
|
||||
case CommandMessage:
|
||||
return "Message"
|
||||
case CommandNewGame:
|
||||
return "NewGame"
|
||||
case CommandJoinGame:
|
||||
return "JoinGame"
|
||||
case CommandQuitGame:
|
||||
return "QuitGame"
|
||||
case CommandUpdateGame:
|
||||
return "UpdateGame"
|
||||
case CommandStartGame:
|
||||
return "StartGame"
|
||||
case CommandGameOver:
|
||||
return "GameOver"
|
||||
case CommandUpdateMatrix:
|
||||
return "UpdateMatrix"
|
||||
case CommandSendGarbage:
|
||||
return "Garbage-OUT"
|
||||
case CommandReceiveGarbage:
|
||||
return "Garbage-IN"
|
||||
case CommandStats:
|
||||
return "Stats"
|
||||
default:
|
||||
return strconv.Itoa(int(c))
|
||||
}
|
||||
}
|
||||
|
||||
type GameCommandInterface interface {
|
||||
Command() Command
|
||||
Source() int
|
||||
SetSource(int)
|
||||
}
|
||||
|
||||
type GameCommand struct {
|
||||
SourcePlayer int
|
||||
}
|
||||
|
||||
func (gc *GameCommand) Source() int {
|
||||
if gc == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return gc.SourcePlayer
|
||||
}
|
||||
|
||||
func (gc *GameCommand) SetSource(source int) {
|
||||
if gc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gc.SourcePlayer = source
|
||||
}
|
||||
|
||||
type GameCommandDisconnect struct {
|
||||
GameCommand
|
||||
Player int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (gc GameCommandDisconnect) Command() Command {
|
||||
return CommandDisconnect
|
||||
}
|
||||
|
||||
type GameCommandPing struct {
|
||||
GameCommand
|
||||
Message string
|
||||
}
|
||||
|
||||
func (gc GameCommandPing) Command() Command {
|
||||
return CommandPing
|
||||
}
|
||||
|
||||
type GameCommandPong struct {
|
||||
GameCommand
|
||||
Message string
|
||||
}
|
||||
|
||||
func (gc GameCommandPong) Command() Command {
|
||||
return CommandPong
|
||||
}
|
||||
|
||||
type GameCommandNickname struct {
|
||||
GameCommand
|
||||
Player int
|
||||
Nickname string
|
||||
}
|
||||
|
||||
func (gc GameCommandNickname) Command() Command {
|
||||
return CommandNickname
|
||||
}
|
||||
|
||||
type GameCommandMessage struct {
|
||||
GameCommand
|
||||
Player int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (gc GameCommandMessage) Command() Command {
|
||||
return CommandMessage
|
||||
}
|
||||
|
||||
type GameCommandJoinGame struct {
|
||||
GameCommand
|
||||
Version int
|
||||
Name string
|
||||
GameID int
|
||||
PlayerID int
|
||||
}
|
||||
|
||||
func (gc GameCommandJoinGame) Command() Command {
|
||||
return CommandJoinGame
|
||||
}
|
||||
|
||||
type GameCommandQuitGame struct {
|
||||
GameCommand
|
||||
Player int
|
||||
}
|
||||
|
||||
func (gc GameCommandQuitGame) Command() Command {
|
||||
return CommandQuitGame
|
||||
}
|
||||
|
||||
type GameCommandUpdateGame struct {
|
||||
GameCommand
|
||||
Players map[int]string
|
||||
}
|
||||
|
||||
func (gc GameCommandUpdateGame) Command() Command {
|
||||
return CommandUpdateGame
|
||||
}
|
||||
|
||||
type GameCommandStartGame struct {
|
||||
GameCommand
|
||||
Seed int64
|
||||
Started bool
|
||||
}
|
||||
|
||||
func (gc GameCommandStartGame) Command() Command {
|
||||
return CommandStartGame
|
||||
}
|
||||
|
||||
type GameCommandUpdateMatrix struct {
|
||||
GameCommand
|
||||
Matrixes map[int]*mino.Matrix
|
||||
}
|
||||
|
||||
func (gc GameCommandUpdateMatrix) Command() Command {
|
||||
return CommandUpdateMatrix
|
||||
}
|
||||
|
||||
type GameCommandGameOver struct {
|
||||
GameCommand
|
||||
Player int
|
||||
Winner string
|
||||
}
|
||||
|
||||
func (gc GameCommandGameOver) Command() Command {
|
||||
return CommandGameOver
|
||||
}
|
||||
|
||||
type GameCommandSendGarbage struct {
|
||||
GameCommand
|
||||
Lines int
|
||||
}
|
||||
|
||||
func (gc GameCommandSendGarbage) Command() Command {
|
||||
return CommandSendGarbage
|
||||
}
|
||||
|
||||
type GameCommandReceiveGarbage struct {
|
||||
GameCommand
|
||||
Lines int
|
||||
}
|
||||
|
||||
func (gc GameCommandReceiveGarbage) Command() Command {
|
||||
return CommandReceiveGarbage
|
||||
}
|
||||
|
||||
type GameCommandStats struct {
|
||||
GameCommand
|
||||
Created time.Time
|
||||
Players int
|
||||
Games int
|
||||
}
|
||||
|
||||
func (gc GameCommandStats) Command() Command {
|
||||
return CommandStats
|
||||
}
|
363
pkg/game/conn.go
Normal file
363
pkg/game/conn.go
Normal file
|
@ -0,0 +1,363 @@
|
|||
package game
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~tslocum/netris/pkg/event"
|
||||
)
|
||||
|
||||
const ConnTimeout = 30 * time.Second
|
||||
|
||||
type GameCommandTransport struct {
|
||||
Command Command `json:"cmd"`
|
||||
Data json.RawMessage
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
conn net.Conn
|
||||
LastTransfer time.Time
|
||||
Terminated bool
|
||||
|
||||
Player int
|
||||
In chan GameCommandInterface
|
||||
out chan GameCommandInterface
|
||||
forwardOut chan GameCommandInterface
|
||||
|
||||
*sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewServerConn(conn net.Conn, forwardOut chan GameCommandInterface) *Conn {
|
||||
c := Conn{conn: conn, WaitGroup: new(sync.WaitGroup)}
|
||||
|
||||
c.In = make(chan GameCommandInterface, CommandQueueSize)
|
||||
c.out = make(chan GameCommandInterface, CommandQueueSize)
|
||||
c.forwardOut = forwardOut
|
||||
|
||||
c.LastTransfer = time.Now()
|
||||
|
||||
if conn == nil {
|
||||
// Local instance
|
||||
|
||||
go c.handleLocalWrite()
|
||||
} else {
|
||||
go c.handleRead()
|
||||
go c.handleWrite()
|
||||
go c.handleSendKeepAlive()
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func Connect(address string) *Conn {
|
||||
var (
|
||||
network string
|
||||
conn net.Conn
|
||||
err error
|
||||
tries int
|
||||
)
|
||||
network, address = NetworkAndAddress(address)
|
||||
|
||||
for {
|
||||
conn, err = net.DialTimeout(network, address, ConnTimeout)
|
||||
if err != nil {
|
||||
if tries > 25 {
|
||||
log.Fatalf("failed to connect to %s: %s", address, err)
|
||||
} else {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
tries++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return NewServerConn(conn, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Conn) handleSendKeepAlive() {
|
||||
t := time.NewTicker(7 * time.Second)
|
||||
for {
|
||||
<-t.C
|
||||
|
||||
if s.Terminated {
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Only send when necessary
|
||||
s.Write(&GameCommandPing{Message: fmt.Sprintf("a%d", time.Now().UnixNano())})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Conn) Write(gc GameCommandInterface) {
|
||||
if s == nil || s.Terminated {
|
||||
return
|
||||
}
|
||||
|
||||
s.Add(1)
|
||||
s.out <- gc
|
||||
}
|
||||
|
||||
func (s *Conn) handleLocalWrite() {
|
||||
for e := range s.out {
|
||||
if s.forwardOut != nil {
|
||||
s.forwardOut <- e
|
||||
}
|
||||
|
||||
s.Done()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Conn) addSourceID(gc GameCommandInterface) {
|
||||
gc.SetSource(s.Player)
|
||||
}
|
||||
|
||||
func (s *Conn) handleRead() {
|
||||
if s.conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := s.conn.SetReadDeadline(time.Now().Add(ConnTimeout))
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
msg GameCommandTransport
|
||||
gc GameCommandInterface
|
||||
processed bool
|
||||
|
||||
um = func(mgc interface{}) {
|
||||
err := json.Unmarshal(msg.Data, mgc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
scanner := bufio.NewScanner(s.conn)
|
||||
for scanner.Scan() {
|
||||
processed = false
|
||||
|
||||
err := json.Unmarshal(scanner.Bytes(), &msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.LastTransfer = time.Now()
|
||||
|
||||
switch msg.Command {
|
||||
case CommandDisconnect:
|
||||
var mgc GameCommandDisconnect
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandPing:
|
||||
var mgc GameCommandPing
|
||||
um(&mgc)
|
||||
|
||||
s.Write(&GameCommandPong{Message: mgc.Message})
|
||||
processed = true
|
||||
case CommandPong:
|
||||
var mgc GameCommandPong
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandMessage:
|
||||
var mgc GameCommandMessage
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandNickname:
|
||||
var mgc GameCommandNickname
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandJoinGame:
|
||||
var mgc GameCommandJoinGame
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandQuitGame:
|
||||
var mgc GameCommandQuitGame
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandUpdateGame:
|
||||
var mgc GameCommandUpdateGame
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandStartGame:
|
||||
var mgc GameCommandStartGame
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandGameOver:
|
||||
var mgc GameCommandGameOver
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandUpdateMatrix:
|
||||
var mgc GameCommandUpdateMatrix
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandSendGarbage:
|
||||
var mgc GameCommandSendGarbage
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandReceiveGarbage:
|
||||
var mgc GameCommandReceiveGarbage
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
case CommandStats:
|
||||
var mgc GameCommandStats
|
||||
um(&mgc)
|
||||
gc = &mgc
|
||||
default:
|
||||
log.Println("unknown serverconn command", scanner.Text())
|
||||
continue
|
||||
}
|
||||
|
||||
if !processed {
|
||||
s.addSourceID(gc)
|
||||
s.In <- gc
|
||||
}
|
||||
|
||||
err = s.conn.SetReadDeadline(time.Now().Add(ConnTimeout))
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.Close()
|
||||
}
|
||||
|
||||
func (s *Conn) handleWrite() {
|
||||
if s.conn == nil {
|
||||
for range s.out {
|
||||
s.Done()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
msg GameCommandTransport
|
||||
j []byte
|
||||
err error
|
||||
)
|
||||
|
||||
for e := range s.out {
|
||||
if s.Terminated {
|
||||
s.Done()
|
||||
continue
|
||||
}
|
||||
|
||||
msg = GameCommandTransport{Command: e.Command()}
|
||||
|
||||
msg.Data, err = json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
j, err = json.Marshal(msg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
j = append(j, '\n')
|
||||
|
||||
err = s.conn.SetWriteDeadline(time.Now().Add(ConnTimeout))
|
||||
if err != nil {
|
||||
s.Close()
|
||||
}
|
||||
|
||||
_, err = s.conn.Write(j)
|
||||
if err != nil {
|
||||
s.Close()
|
||||
}
|
||||
|
||||
s.LastTransfer = time.Now()
|
||||
s.conn.SetWriteDeadline(time.Time{})
|
||||
s.Done()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Conn) Close() {
|
||||
if s.Terminated {
|
||||
return
|
||||
}
|
||||
|
||||
s.Terminated = true
|
||||
|
||||
go func() {
|
||||
s.conn.Close()
|
||||
s.Wait()
|
||||
close(s.In)
|
||||
close(s.out)
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Conn) JoinGame(name string, gameID int, logger chan string, draw chan event.DrawObject) (*Game, error) {
|
||||
s.Write(&GameCommandJoinGame{Name: name, GameID: gameID})
|
||||
var (
|
||||
g *Game
|
||||
err error
|
||||
)
|
||||
|
||||
for e := range s.In {
|
||||
//log.Printf("Receive JoinGame command %+v", e)
|
||||
|
||||
switch e.Command() {
|
||||
case CommandMessage:
|
||||
if p, ok := e.(*GameCommandMessage); ok {
|
||||
prefix := "* "
|
||||
if p.Player > 0 {
|
||||
name := "Anonymous"
|
||||
if player, ok := g.Players[p.Player]; ok {
|
||||
name = player.Name
|
||||
}
|
||||
prefix = "<" + name + "> "
|
||||
}
|
||||
|
||||
if g != nil {
|
||||
g.Log(LogStandard, prefix+p.Message)
|
||||
} else {
|
||||
logger <- prefix + p.Message
|
||||
}
|
||||
}
|
||||
case CommandJoinGame:
|
||||
if p, ok := e.(*GameCommandJoinGame); ok {
|
||||
g, err = NewGame(4, s.Write, logger, draw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.Lock()
|
||||
g.LocalPlayer = p.PlayerID
|
||||
g.Unlock()
|
||||
}
|
||||
case CommandUpdateGame:
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if p, ok := e.(*GameCommandUpdateGame); ok {
|
||||
g.processUpdateGame(p)
|
||||
}
|
||||
case CommandStartGame:
|
||||
if p, ok := e.(*GameCommandStartGame); ok {
|
||||
if g != nil {
|
||||
g.Start(p.Seed)
|
||||
|
||||
if p.Started {
|
||||
g.Players[g.LocalPlayer].Matrix.GameOver = true
|
||||
}
|
||||
|
||||
go g.HandleReadCommands(s.In)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
143
pkg/game/game.go
143
pkg/game/game.go
|
@ -12,7 +12,11 @@ import (
|
|||
"git.sr.ht/~tslocum/netris/pkg/mino"
|
||||
)
|
||||
|
||||
const UpdateDuration = 850 * time.Millisecond
|
||||
const (
|
||||
UpdateDuration = 850 * time.Millisecond
|
||||
IdleStart = 5 * time.Second
|
||||
IdleTimeout = 1 * time.Minute
|
||||
)
|
||||
|
||||
const (
|
||||
LogStandard = iota
|
||||
|
@ -20,36 +24,34 @@ const (
|
|||
LogVerbose
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultPort = 1984
|
||||
DefaultServer = "netris.rocketnine.space"
|
||||
)
|
||||
var Version string
|
||||
|
||||
type Game struct {
|
||||
Rank int
|
||||
Minos []mino.Mino
|
||||
Seed int64
|
||||
Players map[int]*Player
|
||||
FallTime time.Duration
|
||||
ID int
|
||||
|
||||
Event chan interface{}
|
||||
|
||||
out func(GameCommandInterface)
|
||||
|
||||
Started bool
|
||||
Starting bool
|
||||
GameOver bool
|
||||
SentGameOverMatrix bool
|
||||
Started bool
|
||||
TimeStarted time.Time
|
||||
gameOver bool
|
||||
sentGameOverMatrix bool
|
||||
Terminated bool
|
||||
|
||||
Local bool
|
||||
LocalPlayer int
|
||||
NextPlayer int
|
||||
nextPlayer int
|
||||
Players map[int]*Player
|
||||
|
||||
Event chan interface{}
|
||||
out func(GameCommandInterface)
|
||||
|
||||
draw chan event.DrawObject
|
||||
logger chan string
|
||||
LogLevel int
|
||||
|
||||
Local bool
|
||||
Rank int
|
||||
Minos []mino.Mino
|
||||
Seed int64
|
||||
FallTime time.Duration
|
||||
|
||||
sentPing time.Time
|
||||
*sync.Mutex
|
||||
|
@ -77,8 +79,8 @@ func NewGame(rank int, out func(GameCommandInterface), logger chan string, draw
|
|||
g := &Game{
|
||||
Rank: rank,
|
||||
Minos: minos,
|
||||
nextPlayer: 1,
|
||||
Players: make(map[int]*Player),
|
||||
NextPlayer: 1,
|
||||
Event: make(chan interface{}, CommandQueueSize),
|
||||
draw: draw,
|
||||
logger: logger,
|
||||
|
@ -104,7 +106,6 @@ func (g *Game) Log(level int, a ...interface{}) {
|
|||
}
|
||||
|
||||
g.logger <- fmt.Sprint(a...)
|
||||
g.draw <- event.DrawMessages
|
||||
}
|
||||
|
||||
func (g *Game) Logf(level int, format string, a ...interface{}) {
|
||||
|
@ -124,9 +125,12 @@ func (g *Game) AddPlayer(p *Player) {
|
|||
|
||||
func (g *Game) AddPlayerL(p *Player) {
|
||||
if p.Player == PlayerUnknown {
|
||||
p.Player = g.NextPlayer
|
||||
if g.LocalPlayer != PlayerHost {
|
||||
return
|
||||
}
|
||||
|
||||
g.NextPlayer++
|
||||
p.Player = g.nextPlayer
|
||||
g.nextPlayer++
|
||||
}
|
||||
|
||||
g.Players[p.Player] = p
|
||||
|
@ -171,11 +175,14 @@ func (g *Game) RemovePlayer(playerID int) {
|
|||
func (g *Game) RemovePlayerL(playerID int) {
|
||||
if playerID < 0 {
|
||||
return
|
||||
} else if _, ok := g.Players[playerID]; !ok {
|
||||
}
|
||||
|
||||
p, ok := g.Players[playerID]
|
||||
if !ok || p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
playerName := g.Players[playerID].Name
|
||||
playerName := p.Name
|
||||
|
||||
delete(g.Players, playerID)
|
||||
|
||||
|
@ -230,11 +237,12 @@ func (g *Game) Start(seed int64) int64 {
|
|||
func (g *Game) StartL(seed int64) int64 {
|
||||
restarting := g.Seed != 0
|
||||
|
||||
if g.GameOver || g.Started {
|
||||
if g.gameOver || g.Started {
|
||||
return g.Seed
|
||||
}
|
||||
|
||||
g.Started = true
|
||||
g.TimeStarted = time.Now()
|
||||
|
||||
if g.LocalPlayer == PlayerUnknown {
|
||||
panic("Player unknown")
|
||||
|
@ -303,8 +311,9 @@ func (g *Game) ResetL() {
|
|||
|
||||
g.Starting = false
|
||||
g.Started = false
|
||||
g.GameOver = false
|
||||
g.SentGameOverMatrix = false
|
||||
g.TimeStarted = time.Time{}
|
||||
g.setGameOverL(false)
|
||||
g.sentGameOverMatrix = false
|
||||
|
||||
for _, p := range g.Players {
|
||||
p.totalGarbageSent = 0
|
||||
|
@ -328,11 +337,11 @@ func (g *Game) StopL() {
|
|||
return
|
||||
}
|
||||
|
||||
g.Terminated = true
|
||||
|
||||
for playerID := range g.Players {
|
||||
g.RemovePlayerL(playerID)
|
||||
}
|
||||
|
||||
g.Terminated = true
|
||||
}
|
||||
|
||||
func (g *Game) handleSendMatrix() {
|
||||
|
@ -346,7 +355,7 @@ func (g *Game) handleSendMatrix() {
|
|||
|
||||
g.Lock()
|
||||
|
||||
if !g.Started || (g.SentGameOverMatrix && m.GameOver) {
|
||||
if !g.Started || (g.sentGameOverMatrix && m.GameOver) {
|
||||
g.Unlock()
|
||||
continue
|
||||
}
|
||||
|
@ -356,7 +365,7 @@ func (g *Game) handleSendMatrix() {
|
|||
g.out(&GameCommandUpdateMatrix{Matrixes: matrixes})
|
||||
|
||||
if m.GameOver {
|
||||
g.SentGameOverMatrix = true
|
||||
g.sentGameOverMatrix = true
|
||||
}
|
||||
|
||||
g.Unlock()
|
||||
|
@ -379,20 +388,35 @@ func (g *Game) handleDistributeMatrixes() {
|
|||
remainingPlayer := -1
|
||||
remainingPlayers := 0
|
||||
|
||||
for playerID := range g.Players {
|
||||
if g.Players[playerID].Terminated {
|
||||
for playerID, p := range g.Players {
|
||||
if p.Terminated {
|
||||
g.RemovePlayerL(playerID)
|
||||
continue
|
||||
}
|
||||
|
||||
if !g.GameOver && !g.Players[playerID].Matrix.GameOver {
|
||||
if !g.gameOver && !p.Matrix.GameOver && !g.Local && time.Since(p.Moved) >= IdleStart && time.Since(g.TimeStarted) >= IdleStart {
|
||||
p.Idle += UpdateDuration
|
||||
if p.Idle >= IdleTimeout {
|
||||
// Disconnect idle player
|
||||
p.Write(&GameCommandDisconnect{Player: playerID, Message: "Idling is not allowed"})
|
||||
g.RemovePlayerL(playerID)
|
||||
|
||||
p := p
|
||||
go func(p *Player) {
|
||||
time.Sleep(time.Second)
|
||||
p.Close()
|
||||
}(p)
|
||||
}
|
||||
}
|
||||
|
||||
if !g.gameOver && !p.Matrix.GameOver {
|
||||
remainingPlayer = playerID
|
||||
remainingPlayers++
|
||||
}
|
||||
}
|
||||
|
||||
if !g.GameOver && !g.Local && remainingPlayers <= 1 {
|
||||
g.GameOver = true
|
||||
if !g.gameOver && !g.Local && remainingPlayers <= 1 {
|
||||
g.setGameOverL(true)
|
||||
|
||||
winner := "Tie!"
|
||||
if remainingPlayer != -1 {
|
||||
|
@ -464,12 +488,24 @@ func (g *Game) HandleReadCommands(in chan GameCommandInterface) {
|
|||
g.Log(logLevel, "LOCAL handle ", e.Command(), " from ", e.Source(), " ", e)
|
||||
|
||||
switch e.Command() {
|
||||
case CommandDisconnect:
|
||||
if p, ok := e.(*GameCommandDisconnect); ok {
|
||||
if p.Player == g.LocalPlayer {
|
||||
if p.Message != "" {
|
||||
g.Logf(LogStandard, "* Disconnected - Reason: %s", p.Message)
|
||||
} else {
|
||||
g.Logf(LogStandard, "* Disconnected")
|
||||
}
|
||||
|
||||
g.setGameOverL(true)
|
||||
}
|
||||
}
|
||||
case CommandPong:
|
||||
if p, ok := e.(*GameCommandPong); ok {
|
||||
if len(p.Message) > 1 && p.Message[0] == 'm' {
|
||||
if i, err := strconv.ParseInt(p.Message[1:], 10, 64); err == nil {
|
||||
if i == g.sentPing.UnixNano() {
|
||||
g.Logf(LogStandard, "Server latency is %dms", time.Since(g.sentPing).Milliseconds())
|
||||
g.Logf(LogStandard, "* Server latency is %dms", time.Since(g.sentPing).Milliseconds())
|
||||
|
||||
g.sentPing = time.Time{}
|
||||
}
|
||||
|
@ -547,19 +583,18 @@ func (g *Game) HandleReadCommands(in chan GameCommandInterface) {
|
|||
case CommandGameOver:
|
||||
if p, ok := e.(*GameCommandGameOver); ok {
|
||||
if p.Winner != "" {
|
||||
g.GameOver = true
|
||||
|
||||
for _, p := range g.Players {
|
||||
p.Matrix.SetGameOver()
|
||||
}
|
||||
|
||||
g.draw <- event.DrawAll
|
||||
g.setGameOverL(true)
|
||||
} else {
|
||||
g.Players[p.Player].Matrix.SetGameOver()
|
||||
|
||||
g.draw <- event.DrawMultiplayerMatrixes
|
||||
}
|
||||
}
|
||||
case CommandStats:
|
||||
if p, ok := e.(*GameCommandStats); ok {
|
||||
g.Logf(LogStandard, "* %d players in %d games - uptime: %s", p.Players, p.Games, time.Since(p.Created.Local()).Truncate(time.Minute))
|
||||
|
||||
}
|
||||
default:
|
||||
g.Log(LogStandard, "unknown handle read command", e.Command(), e)
|
||||
}
|
||||
|
@ -568,6 +603,22 @@ func (g *Game) HandleReadCommands(in chan GameCommandInterface) {
|
|||
}
|
||||
}
|
||||
|
||||
func (g *Game) setGameOverL(gameOver bool) {
|
||||
if g.gameOver == gameOver {
|
||||
return
|
||||
}
|
||||
|
||||
g.gameOver = gameOver
|
||||
|
||||
if g.gameOver {
|
||||
for _, p := range g.Players {
|
||||
p.Matrix.SetGameOver()
|
||||
}
|
||||
|
||||
g.draw <- event.DrawAll
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) handleDistributeGarbage() {
|
||||
t := time.NewTicker(500 * time.Millisecond)
|
||||
for {
|
||||
|
@ -703,6 +754,8 @@ func (g *Game) ProcessAction(a event.GameAction) {
|
|||
case event.ActionPing:
|
||||
g.sentPing = time.Now()
|
||||
g.out(&GameCommandPing{Message: fmt.Sprintf("m%d", g.sentPing.UnixNano())})
|
||||
case event.ActionStats:
|
||||
g.out(&GameCommandStats{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package game
|
|||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~tslocum/netris/pkg/mino"
|
||||
)
|
||||
|
@ -14,293 +14,38 @@ const (
|
|||
PlayerUnknown = 0
|
||||
)
|
||||
|
||||
var nickRegexp = regexp.MustCompile(`[^a-zA-Z0-9_\-!@#$%^&*+=,./]+`)
|
||||
|
||||
type ConnectingPlayer struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
Name string
|
||||
|
||||
*ServerConn
|
||||
*Conn
|
||||
|
||||
Score int
|
||||
Preview *mino.Matrix
|
||||
Matrix *mino.Matrix
|
||||
Moved time.Time // Time of last piece move
|
||||
Idle time.Duration // Time spent idling
|
||||
|
||||
pendingGarbage int
|
||||
totalGarbageSent int
|
||||
totalGarbageReceived int
|
||||
}
|
||||
|
||||
type ConnectingPlayer struct {
|
||||
Client ClientInterface
|
||||
|
||||
Name string
|
||||
}
|
||||
|
||||
func NewPlayer(name string, conn *ServerConn) *Player {
|
||||
/*in := make(chan *GameCommand, CommandQueueSize)
|
||||
out := make(chan *GameCommand, CommandQueueSize)
|
||||
|
||||
p := &Player{Conn: conn, In: in, Out: out}
|
||||
|
||||
go p.handleRead()
|
||||
go p.handleWrite()*/
|
||||
|
||||
func NewPlayer(name string, conn *Conn) *Player {
|
||||
if conn == nil {
|
||||
conn = &ServerConn{}
|
||||
conn = &Conn{}
|
||||
}
|
||||
|
||||
p := &Player{Name: Nickname(name), ServerConn: conn}
|
||||
p := &Player{Name: Nickname(name), Conn: conn, Moved: time.Now()}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
/*
|
||||
func (p *Player) handleRead() {
|
||||
if p.Conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(p.Conn)
|
||||
for scanner.Scan() {
|
||||
log.Println("unmarshal [" + scanner.Text() + "]")
|
||||
|
||||
var gameCommand GameCommand
|
||||
err := json.Unmarshal(scanner.Bytes(), &gameCommand)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p.In <- &gameCommand
|
||||
|
||||
log.Println("read player ")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) handleWrite() {
|
||||
if p.Conn == nil {
|
||||
for range p.Out {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
j []byte
|
||||
err error
|
||||
)
|
||||
for e := range p.Out {
|
||||
j, err = json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Printf("attempting to marshal %+v", e)
|
||||
panic(err)
|
||||
}
|
||||
j = append(j, '\n')
|
||||
_, err = p.Conn.Write(j)
|
||||
if err != nil {
|
||||
p.Conn.Close()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
type ClientInterface interface {
|
||||
Attach(in chan<- GameCommandInterface, out <-chan GameCommandInterface)
|
||||
Detach(reason string)
|
||||
}
|
||||
|
||||
type Command int
|
||||
|
||||
func (c Command) String() string {
|
||||
switch c {
|
||||
case CommandUnknown:
|
||||
return "Unknown"
|
||||
case CommandDisconnect:
|
||||
return "Disconnect"
|
||||
case CommandNickname:
|
||||
return "Nickname"
|
||||
case CommandMessage:
|
||||
return "Message"
|
||||
case CommandNewGame:
|
||||
return "NewGame"
|
||||
case CommandJoinGame:
|
||||
return "JoinGame"
|
||||
case CommandQuitGame:
|
||||
return "QuitGame"
|
||||
case CommandUpdateGame:
|
||||
return "UpdateGame"
|
||||
case CommandStartGame:
|
||||
return "StartGame"
|
||||
case CommandGameOver:
|
||||
return "GameOver"
|
||||
case CommandUpdateMatrix:
|
||||
return "UpdateMatrix"
|
||||
case CommandSendGarbage:
|
||||
return "Garbage-OUT"
|
||||
case CommandReceiveGarbage:
|
||||
return "Garbage-IN"
|
||||
default:
|
||||
return strconv.Itoa(int(c))
|
||||
}
|
||||
}
|
||||
|
||||
// The order of these constants must be preserved
|
||||
const (
|
||||
CommandUnknown Command = iota
|
||||
CommandDisconnect
|
||||
CommandPing
|
||||
CommandPong
|
||||
CommandNickname
|
||||
CommandMessage
|
||||
CommandNewGame
|
||||
CommandJoinGame
|
||||
CommandQuitGame
|
||||
CommandUpdateGame
|
||||
CommandStartGame
|
||||
CommandGameOver
|
||||
CommandUpdateMatrix
|
||||
CommandSendGarbage
|
||||
CommandReceiveGarbage
|
||||
)
|
||||
|
||||
type GameCommand struct {
|
||||
SourcePlayer int
|
||||
}
|
||||
|
||||
func (gc *GameCommand) Source() int {
|
||||
if gc == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return gc.SourcePlayer
|
||||
}
|
||||
|
||||
func (gc *GameCommand) SetSource(source int) {
|
||||
if gc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gc.SourcePlayer = source
|
||||
}
|
||||
|
||||
type GameCommandInterface interface {
|
||||
Command() Command
|
||||
Source() int
|
||||
SetSource(int)
|
||||
}
|
||||
|
||||
type GameCommandPing struct {
|
||||
GameCommand
|
||||
Message string
|
||||
}
|
||||
|
||||
func (gc GameCommandPing) Command() Command {
|
||||
return CommandPing
|
||||
}
|
||||
|
||||
type GameCommandPong struct {
|
||||
GameCommand
|
||||
Message string
|
||||
}
|
||||
|
||||
func (gc GameCommandPong) Command() Command {
|
||||
return CommandPong
|
||||
}
|
||||
|
||||
type GameCommandMessage struct {
|
||||
GameCommand
|
||||
Player int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (gc GameCommandMessage) Command() Command {
|
||||
return CommandMessage
|
||||
}
|
||||
|
||||
type GameCommandJoinGame struct {
|
||||
GameCommand
|
||||
Name string
|
||||
GameID int
|
||||
PlayerID int
|
||||
}
|
||||
|
||||
func (gc GameCommandJoinGame) Command() Command {
|
||||
return CommandJoinGame
|
||||
}
|
||||
|
||||
type GameCommandNickname struct {
|
||||
GameCommand
|
||||
|
||||
Player int
|
||||
Nickname string
|
||||
}
|
||||
|
||||
func (gc GameCommandNickname) Command() Command {
|
||||
return CommandNickname
|
||||
}
|
||||
|
||||
type GameCommandQuitGame struct {
|
||||
GameCommand
|
||||
Player int
|
||||
}
|
||||
|
||||
func (gc GameCommandQuitGame) Command() Command {
|
||||
return CommandQuitGame
|
||||
}
|
||||
|
||||
type GameCommandUpdateGame struct {
|
||||
GameCommand
|
||||
Players map[int]string
|
||||
}
|
||||
|
||||
func (gc GameCommandUpdateGame) Command() Command {
|
||||
return CommandUpdateGame
|
||||
}
|
||||
|
||||
type GameCommandStartGame struct {
|
||||
GameCommand
|
||||
Seed int64
|
||||
Started bool
|
||||
}
|
||||
|
||||
func (gc GameCommandStartGame) Command() Command {
|
||||
return CommandStartGame
|
||||
}
|
||||
|
||||
type GameCommandUpdateMatrix struct {
|
||||
GameCommand
|
||||
Matrixes map[int]*mino.Matrix
|
||||
}
|
||||
|
||||
func (gc GameCommandUpdateMatrix) Command() Command {
|
||||
return CommandUpdateMatrix
|
||||
}
|
||||
|
||||
type GameCommandGameOver struct {
|
||||
GameCommand
|
||||
Player int
|
||||
Winner string
|
||||
}
|
||||
|
||||
func (gc GameCommandGameOver) Command() Command {
|
||||
return CommandGameOver
|
||||
}
|
||||
|
||||
type GameCommandSendGarbage struct {
|
||||
GameCommand
|
||||
Lines int
|
||||
}
|
||||
|
||||
func (gc GameCommandSendGarbage) Command() Command {
|
||||
return CommandSendGarbage
|
||||
}
|
||||
|
||||
type GameCommandReceiveGarbage struct {
|
||||
GameCommand
|
||||
Lines int
|
||||
}
|
||||
|
||||
func (gc GameCommandReceiveGarbage) Command() Command {
|
||||
return CommandReceiveGarbage
|
||||
}
|
||||
|
||||
var nickRegexp = regexp.MustCompile(`[^a-zA-Z0-9_\-!@#$%^&*+=,./]+`)
|
||||
|
||||
func Nickname(nick string) string {
|
||||
nick = nickRegexp.ReplaceAllString(nick, "")
|
||||
if len(nick) > 10 {
|
||||
|
|
|
@ -11,25 +11,32 @@ import (
|
|||
"git.sr.ht/~tslocum/netris/pkg/event"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultPort = 1984
|
||||
DefaultServer = "netris.rocketnine.space"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
I []ServerInterface
|
||||
|
||||
In chan GameCommandInterface
|
||||
Out chan GameCommandInterface
|
||||
|
||||
NewPlayers chan *IncomingPlayer
|
||||
|
||||
Games map[int]*Game
|
||||
|
||||
Logger chan string
|
||||
|
||||
listeners []net.Listener
|
||||
listeners []net.Listener
|
||||
NewPlayers chan *IncomingPlayer
|
||||
|
||||
created time.Time
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type IncomingPlayer struct {
|
||||
Name string
|
||||
Conn *ServerConn
|
||||
Conn *Conn
|
||||
}
|
||||
|
||||
type ServerInterface interface {
|
||||
|
@ -42,12 +49,12 @@ func NewServer(si []ServerInterface) *Server {
|
|||
in := make(chan GameCommandInterface, CommandQueueSize)
|
||||
out := make(chan GameCommandInterface, CommandQueueSize)
|
||||
|
||||
s := &Server{I: si, In: in, Out: out, Games: make(map[int]*Game)}
|
||||
s := &Server{I: si, In: in, Out: out, Games: make(map[int]*Game), created: time.Now()}
|
||||
|
||||
s.NewPlayers = make(chan *IncomingPlayer, CommandQueueSize)
|
||||
|
||||
go s.accept()
|
||||
|
||||
go s.handle()
|
||||
for _, serverInterface := range si {
|
||||
serverInterface.Host(s.NewPlayers)
|
||||
}
|
||||
|
@ -83,11 +90,34 @@ func (s *Server) NewGame() (*Game, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
g.ID = gameID
|
||||
|
||||
s.Games[gameID] = g
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (s *Server) handle() {
|
||||
for {
|
||||
time.Sleep(1 * time.Minute)
|
||||
|
||||
s.Lock()
|
||||
s.removeTerminatedGames()
|
||||
s.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) removeTerminatedGames() {
|
||||
for gameID, g := range s.Games {
|
||||
if g != nil && !g.Terminated {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(s.Games, gameID)
|
||||
g = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) FindGame(p *Player, gameID int) *Game {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
@ -97,26 +127,24 @@ func (s *Server) FindGame(p *Player, gameID int) *Game {
|
|||
err error
|
||||
)
|
||||