Kick idle players

This commit is contained in:
Trevor Slocum 2019-10-21 09:43:09 -07:00
parent 40374caa22
commit 1508f25631
15 changed files with 827 additions and 816 deletions

5
CHANGELOG.md Normal file
View File

@ -0,0 +1,5 @@
0.1.1:
- Kick inactive players
0.1.0:
- Initial release

View File

@ -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)
}
}()

View File

@ -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()
}

View File

@ -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}

View File

@ -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)

View File

@ -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:

View File

@ -11,4 +11,5 @@ const (
ActionSoftDrop
ActionHardDrop
ActionPing
ActionStats
)

View File

@ -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
View 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
View 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
}

View File

@ -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{})
}
}
}

View File

@ -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 {

View File

@ -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
)
if gm, ok := s.Games[gameID]; ok {
g = gm
// Join a game by its ID
if gameID > 0 {
if gm, ok := s.Games[gameID]; ok && !gm.Terminated {
g = gm
}
}
// Join any game
if g == nil {
for gameID, g = range s.Games {
if g != nil {
if g.Terminated {
delete(s.Games, gameID)
g = nil
s.Log("Cleaned up game ", gameID)
continue
}
for _, gm := range s.Games {
if gm != nil && !gm.Terminated {
g = gm
break
}
}
}
// Create a new game
if g == nil {
g, err = s.NewGame()
if err != nil {
@ -165,7 +193,7 @@ func (s *Server) handleJoinGame(pl *Player) {
g := s.FindGame(pl, p.GameID)
s.Logf("Adding %s to game %d", pl.Name, p.GameID)
s.Logf("Adding %s to game %d", pl.Name, g.ID)
go s.handleGameCommands(pl, g)
return
@ -227,8 +255,17 @@ func (s *Server) handleGameCommands(pl *Player, g *Game) {
}
case CommandUpdateMatrix:
if p, ok := e.(*GameCommandUpdateMatrix); ok {
for _, m := range p.Matrixes {
g.Players[p.SourcePlayer].Matrix.Replace(m)
if pl, ok := g.Players[p.SourcePlayer]; ok {
for _, m := range p.Matrixes {
pl.Matrix.Replace(m)
}
m := pl.Matrix
spawn := m.SpawnLocation(m.P)
if m.P != nil && spawn.X >= 0 && spawn.Y >= 0 && m.P.X != spawn.X {
pl.Moved = time.Now()
pl.Idle = 0
}
}
}
case CommandGameOver:
@ -260,6 +297,18 @@ func (s *Server) handleGameCommands(pl *Player, g *Game) {
g.Players[p.SourcePlayer].totalGarbageSent += p.Lines
}
}
case CommandStats:
if p, ok := e.(*GameCommandStats); ok {
players := 0
games := 0
for _, g := range s.Games {
players += len(g.Players)
games++
}
g.Players[p.SourcePlayer].Write(&GameCommandStats{Created: s.created, Players: players, Games: games})
}
}
g.Unlock()

View File

@ -1,455 +0,0 @@
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 ServerConn struct {
Conn net.Conn
Player int
In chan GameCommandInterface
out chan GameCommandInterface
ForwardOut chan GameCommandInterface
LastTransfer time.Time
Terminated bool
*sync.WaitGroup
}
func NewServerConn(conn net.Conn, forwardOut chan GameCommandInterface) *ServerConn {
s := ServerConn{Conn: conn, WaitGroup: new(sync.WaitGroup)}
s.In = make(chan GameCommandInterface, CommandQueueSize)
s.out = make(chan GameCommandInterface, CommandQueueSize)
s.ForwardOut = forwardOut
s.LastTransfer = time.Now()
if conn == nil {
// Local instance
go s.handleLocalWrite()
} else {
go s.handleRead()
go s.handleWrite()
go s.handleSendKeepAlive()
}
return &s
}
func Connect(address string) *ServerConn {
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 *ServerConn) 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 *ServerConn) Write(gc GameCommandInterface) {
if s == nil || s.Terminated {
return
}
s.Add(1)
s.out <- gc
}
func (s *ServerConn) handleLocalWrite() {
for e := range s.out {
if s.ForwardOut != nil {
s.ForwardOut <- e
}
s.Done()
}
}
func (s *ServerConn) addSourceID(gc GameCommandInterface) {
gc.SetSource(s.Player)
}
func (s *ServerConn) 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
)
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()
if msg.Command == CommandPing {
var gameCommand GameCommandPing
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
s.Write(&GameCommandPong{Message: gameCommand.Message})
processed = true
} else if msg.Command == CommandPong {
var gameCommand GameCommandPong
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandMessage {
var gameCommand GameCommandMessage
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandNickname {
var gameCommand GameCommandNickname
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandJoinGame {
var gameCommand GameCommandJoinGame
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandQuitGame {
var gameCommand GameCommandQuitGame
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandUpdateGame {
var gameCommand GameCommandUpdateGame
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandStartGame {
var gameCommand GameCommandStartGame
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandGameOver {
var gameCommand GameCommandGameOver
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandUpdateMatrix {
var gameCommand GameCommandUpdateMatrix
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandSendGarbage {
var gameCommand GameCommandSendGarbage
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else if msg.Command == CommandReceiveGarbage {
var gameCommand GameCommandReceiveGarbage
err := json.Unmarshal(msg.Data, &gameCommand)
if err != nil {
panic(err)
}
gc = &gameCommand
} else {
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 *ServerConn) 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()}
if p, ok := e.(*GameCommandPing); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandPong); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandMessage); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandNickname); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandJoinGame); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandQuitGame); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandUpdateGame); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandStartGame); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandGameOver); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandUpdateMatrix); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandSendGarbage); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else if p, ok := e.(*GameCommandReceiveGarbage); ok {
msg.Data, err = json.Marshal(p)
if err != nil {
panic(err)
}
} else {
log.Println(e.Command(), e)
panic("unknown serverconn write command")
}
j, err = json.Marshal(msg)
if err != nil {
panic(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 *ServerConn) Close() {
if s.Terminated {
return
}
s.Terminated = true
go func() {
s.Conn.Close()
s.Wait()
close(s.In)
close(s.out)
}()
}
func (s *ServerConn) 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
draw <- event.DrawMessages
}
}
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
}

View File

@ -122,12 +122,12 @@ func (m *Matrix) takePiece() bool {
p := NewPiece(m.Bag.Take(), Point{0, 0})
pieceStart := m.pieceStart(p)
if pieceStart.X < 0 || pieceStart.Y < 0 {
spawn := m.SpawnLocation(p)
if spawn.X < 0 || spawn.Y < 0 {
return false
}
p.Point = pieceStart
p.Point = spawn
m.P = p
@ -583,7 +583,11 @@ func (m *Matrix) RotatePiece(rotations int, direction int) bool {
return false
}
func (m *Matrix) pieceStart(p *Piece) Point {
func (m *Matrix) SpawnLocation(p *Piece) Point {
if p == nil {
return Point{-1, -1}
}
w, _ := p.Size()
x := (m.W / 2) - (w / 2)
@ -887,6 +891,10 @@ func (m *Matrix) Replace(newmtx *Matrix) {
m.Lock()
defer m.Unlock()
if m.GameOver && !newmtx.GameOver {
return
}
m.M = newmtx.M
m.P = newmtx.P