Support custom game creation

This commit is contained in:
Trevor Slocum 2019-10-25 17:10:38 -07:00
parent 8196f84ad9
commit 4aa44246f2
18 changed files with 1210 additions and 570 deletions

View File

@ -1,4 +1,6 @@
0.1.1:
- Add game browser and support custom game creation
- Add website and version to title screen
- Kick inactive players
0.1.0:

8
NETWORKING.md Normal file
View File

@ -0,0 +1,8 @@
*This is a draft specification of a future networking protocol.*
# Overview
Gameplay is recorded and buffered for 1-2 seconds before being sent to the server, then distributed among all players.
Each

View File

@ -5,14 +5,16 @@
Multiplayer Tetris clone
![](https://netris.rocketnine.space/static/screenshot4.png)
## Play Without Installing
## Demo
To play netris without installing:
To play netris without installing, connect via [SSH](https://en.wikipedia.org/wiki/Secure_Shell):
```ssh netris.rocketnine.space```
## Screenshot
![](https://netris.rocketnine.space/static/screenshot4.png)
## Install
Choose one of the following methods:

View File

@ -33,8 +33,8 @@ var (
joinedGame bool
draw = make(chan event.DrawObject, game.CommandQueueSize)
selectMode = make(chan event.GameMode, game.CommandQueueSize)
draw = make(chan event.DrawObject, game.CommandQueueSize)
joinGame = make(chan int, game.CommandQueueSize)
renderLock = new(sync.Mutex)
renderBuffer bytes.Buffer
@ -60,6 +60,9 @@ var (
buttonKeybindHardDrop *tview.Button
buttonKeybindCancel *tview.Button
buttonKeybindSave *tview.Button
buttonCancel *tview.Button
buttonStart *tview.Button
)
const DefaultStatusText = "Press Enter to chat, Z/X to rotate, arrow keys or HJKL to move/drop"
@ -87,284 +90,14 @@ var renderBlock = map[mino.Block][]byte{
var (
renderHLine = []byte(string(tcell.RuneHLine))
renderVLine = []byte(string(tcell.RuneVLine))
renderLTee = []byte(string(tcell.RuneLTee))
renderRTee = []byte(string(tcell.RuneRTee))
renderULCorner = []byte(string(tcell.RuneULCorner))
renderURCorner = []byte(string(tcell.RuneURCorner))
renderLLCorner = []byte(string(tcell.RuneLLCorner))
renderLRCorner = []byte(string(tcell.RuneLRCorner))
)
func initGUI(skipTitle bool) (*tview.Application, error) {
app = tview.NewApplication()
app.SetAfterResizeFunc(handleResize)
inputView = tview.NewInputField().
SetText(DefaultStatusText).
SetLabel("> ").
SetFieldWidth(0).
SetFieldBackgroundColor(tcell.ColorDefault).
SetFieldTextColor(tcell.ColorWhite)
inputView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if !inputActive {
return nil
}
return event
})
gameGrid = tview.NewGrid().
SetBorders(false).
SetRows(2+(20*blockSize)+extraScreenPadding, -1)
mtx = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false)
mtx.SetDynamicColors(true)
side = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false)
side.SetDynamicColors(true)
buffer = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false)
buffer.SetDynamicColors(true)
spacer := tview.NewBox()
recent = tview.NewTextView().
SetScrollable(true).
SetTextAlign(tview.AlignLeft).
SetWrap(true).
SetWordWrap(true)
gameGrid.SetColumns(1, 4+(10*blockSize), 10, -1).
AddItem(spacer, 0, 0, 2, 1, 0, 0, false).
AddItem(mtx, 0, 1, 1, 1, 0, 0, false).
AddItem(side, 0, 2, 1, 1, 0, 0, false).
AddItem(buffer, 0, 3, 1, 1, 0, 0, false).
AddItem(inputView, 1, 1, 1, 3, 0, 0, true).
AddItem(recent, 2, 1, 1, 3, 0, 0, true)
// Set up title screen
titleVisible = !skipTitle
minos, err := mino.Generate(4)
if err != nil {
log.Fatalf("failed to render title: failed to generate minos: %s", err)
}
var (
piece *mino.Piece
addToRight bool
i int
)
for y := 0; y < 6; y++ {
for x := 0; x < 4; x++ {
piece = mino.NewPiece(minos[i], mino.Point{x * 5, (y * 5)})
i++
if i == len(minos) {
i = 0
}
if addToRight {
titlePiecesR = append(titlePiecesR, piece)
} else {
titlePiecesL = append(titlePiecesL, piece)
}
addToRight = !addToRight
}
}
titleName = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false).SetDynamicColors(true)
titleL = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false).SetDynamicColors(true)
titleR = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false).SetDynamicColors(true)
go handleTitle()
buttonA = tview.NewButton("A")
buttonLabelA = tview.NewTextView().SetTextAlign(tview.AlignCenter)
buttonB = tview.NewButton("B")
buttonLabelB = tview.NewTextView().SetTextAlign(tview.AlignCenter)
buttonC = tview.NewButton("C")
buttonLabelC = tview.NewTextView().SetTextAlign(tview.AlignCenter)
titleNameGrid := tview.NewGrid().SetRows(5).
AddItem(titleName, 0, 0, 1, 1, 0, 0, false).
AddItem(tview.NewTextView().SetText(SubTitle+game.Version), 1, 0, 1, 1, 0, 0, false)
titleGrid = tview.NewGrid().
SetRows(7, 3, 3, 3, 3, 3, 2).
SetColumns(-1, 38, -1).
AddItem(titleL, 0, 0, 7, 1, 0, 0, false).
AddItem(titleNameGrid, 0, 1, 1, 1, 0, 0, false).
AddItem(titleR, 0, 2, 7, 1, 0, 0, false).
AddItem(buttonA, 1, 1, 1, 1, 0, 0, false).
AddItem(buttonLabelA, 2, 1, 1, 1, 0, 0, false).
AddItem(buttonB, 3, 1, 1, 1, 0, 0, false).
AddItem(buttonLabelB, 4, 1, 1, 1, 0, 0, false).
AddItem(buttonC, 5, 1, 1, 1, 0, 0, false).
AddItem(buttonLabelC, 6, 1, 1, 1, 0, 0, false)
playerSettingsTitle := tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("Player Settings")
playerSettingsForm = tview.NewForm().SetButtonsAlign(tview.AlignCenter)
playerSettingsGrid = tview.NewGrid().
SetRows(7, 2, -1, 1).
SetColumns(-1, 38, -1).
AddItem(titleL, 0, 0, 3, 1, 0, 0, false).
AddItem(titleNameGrid, 0, 1, 1, 1, 0, 0, false).
AddItem(titleR, 0, 2, 3, 1, 0, 0, false).
AddItem(playerSettingsTitle, 1, 1, 1, 1, 0, 0, true).
AddItem(playerSettingsForm, 2, 1, 1, 1, 0, 0, true).
AddItem(tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("Press Tab to move between fields"), 3, 1, 1, 1, 0, 0, true)
gameSettingsTitle := tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("Game Settings")
buttonKeybindRotateCCW = tview.NewButton("Set")
buttonKeybindRotateCW = tview.NewButton("Set")
buttonKeybindMoveLeft = tview.NewButton("Set")
buttonKeybindMoveRight = tview.NewButton("Set")
buttonKeybindSoftDrop = tview.NewButton("Set")
buttonKeybindHardDrop = tview.NewButton("Set")
buttonKeybindCancel = tview.NewButton("Cancel")
buttonKeybindSave = tview.NewButton("Save")
rotateCCWGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Rotate CCW"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindRotateCCW, 0, 1, 1, 1, 0, 0, false)
rotateCWGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Rotate CW"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindRotateCW, 0, 1, 1, 1, 0, 0, false)
moveLeftGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Move Left"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindMoveLeft, 0, 1, 1, 1, 0, 0, false)
moveRightGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Move Right"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindMoveRight, 0, 1, 1, 1, 0, 0, false)
softDropGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Soft Drop"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindSoftDrop, 0, 1, 1, 1, 0, 0, false)
hardDropGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Hard Drop"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindHardDrop, 0, 1, 1, 1, 0, 0, false)
gameSettingsSubmitGrid := tview.NewGrid().
SetColumns(-1, 10, 1, 10, -1).
AddItem(tview.NewTextView(), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindCancel, 0, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 0, 2, 1, 1, 0, 0, false).
AddItem(buttonKeybindSave, 0, 3, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 0, 4, 1, 1, 0, 0, false)
gameSettingsGrid = tview.NewGrid().
SetRows(7, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1).
SetColumns(-1, 38, -1).
AddItem(titleL, 0, 0, 16, 1, 0, 0, false).
AddItem(titleNameGrid, 0, 1, 1, 1, 0, 0, false).
AddItem(titleR, 0, 2, 16, 1, 0, 0, false).
AddItem(gameSettingsTitle, 1, 1, 1, 1, 0, 0, false).
AddItem(rotateCCWGrid, 2, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 3, 1, 1, 1, 0, 0, false).
AddItem(rotateCWGrid, 4, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 5, 1, 1, 1, 0, 0, false).
AddItem(moveLeftGrid, 6, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 7, 1, 1, 1, 0, 0, false).
AddItem(moveRightGrid, 8, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 9, 1, 1, 1, 0, 0, false).
AddItem(softDropGrid, 10, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 11, 1, 1, 1, 0, 0, false).
AddItem(hardDropGrid, 12, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 13, 1, 1, 1, 0, 0, false).
AddItem(gameSettingsSubmitGrid, 14, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("\nPress Tab to move between fields"), 15, 1, 1, 1, 0, 0, false)
titleContainerGrid = tview.NewGrid().SetColumns(-1, 80, -1).SetRows(-1, 24, -1).
AddItem(tview.NewTextView(), 0, 0, 1, 3, 0, 0, false).
AddItem(tview.NewTextView(), 1, 0, 1, 1, 0, 0, false).
AddItem(titleGrid, 1, 1, 1, 1, 0, 0, true).
AddItem(tview.NewTextView(), 1, 2, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 0, 0, 1, 3, 0, 0, false)
playerSettingsContainerGrid = tview.NewGrid().SetColumns(-1, 80, -1).SetRows(-1, 24, -1).
AddItem(tview.NewTextView(), 0, 0, 1, 3, 0, 0, false).
AddItem(tview.NewTextView(), 1, 0, 1, 1, 0, 0, false).
AddItem(playerSettingsGrid, 1, 1, 1, 1, 0, 0, true).
AddItem(tview.NewTextView(), 1, 2, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 0, 0, 1, 3, 0, 0, false)
gameSettingsContainerGrid = tview.NewGrid().SetColumns(-1, 80, -1).SetRows(-1, 24, -1).
AddItem(tview.NewTextView(), 0, 0, 1, 3, 0, 0, false).
AddItem(tview.NewTextView(), 1, 0, 1, 1, 0, 0, false).
AddItem(gameSettingsGrid, 1, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 1, 2, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 0, 0, 1, 3, 0, 0, false)
app = app.SetInputCapture(handleKeypress)
if !skipTitle {
app.SetRoot(titleContainerGrid, true)
updateTitle()
} else {
app.SetRoot(gameGrid, true)
app.SetFocus(nil)
}
go handleDraw()
return app, nil
}
func resetPlayerSettingsForm() {
playerSettingsForm.Clear(true).AddInputField("Name", nickname, 0, nil, func(text string) {
nicknameDraft = text
@ -498,14 +231,14 @@ func setInputStatus(active bool) {
inputActive = active
inputView.SetText("")
if inputActive {
app.SetFocus(inputView)
} else {
app.SetFocus(nil)
}
app.Draw()
app.QueueUpdateDraw(func() {
inputView.SetText("")
if inputActive {
app.SetFocus(inputView)
} else {
app.SetFocus(nil)
}
})
}
func setShowDetails(active bool) {

503
cmd/netris/gui_init.go Normal file
View File

@ -0,0 +1,503 @@
package main
import (
"log"
"unicode"
"git.sr.ht/~tslocum/netris/pkg/event"
"git.sr.ht/~tslocum/netris/pkg/game"
"git.sr.ht/~tslocum/netris/pkg/mino"
"github.com/gdamore/tcell"
"github.com/tslocum/tview"
)
func initGUI(skipTitle bool) (*tview.Application, error) {
app = tview.NewApplication()
app.SetAfterResizeFunc(handleResize)
inputView = tview.NewInputField().
SetText(DefaultStatusText).
SetLabel("> ").
SetFieldWidth(0).
SetFieldBackgroundColor(tcell.ColorDefault).
SetFieldTextColor(tcell.ColorWhite)
inputView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if !inputActive {
return nil
}
return event
})
gameGrid = tview.NewGrid().
SetBorders(false).
SetRows(2+(20*blockSize)+extraScreenPadding, -1)
mtx = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false)
mtx.SetDynamicColors(true)
side = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false)
side.SetDynamicColors(true)
buffer = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false)
buffer.SetDynamicColors(true)
pad := tview.NewBox()
recent = tview.NewTextView().
SetScrollable(true).
SetTextAlign(tview.AlignLeft).
SetWrap(true).
SetWordWrap(true)
gameGrid.SetColumns(1, 4+(10*blockSize), 10, -1).
AddItem(pad, 0, 0, 2, 1, 0, 0, false).
AddItem(mtx, 0, 1, 1, 1, 0, 0, false).
AddItem(side, 0, 2, 1, 1, 0, 0, false).
AddItem(buffer, 0, 3, 1, 1, 0, 0, false).
AddItem(inputView, 1, 1, 1, 3, 0, 0, true).
AddItem(recent, 2, 1, 1, 3, 0, 0, true)
// Set up title screen
titleVisible = !skipTitle
minos, err := mino.Generate(4)
if err != nil {
log.Fatalf("failed to render title: failed to generate minos: %s", err)
}
var (
piece *mino.Piece
addToRight bool
i int
)
for y := 0; y < 6; y++ {
for x := 0; x < 4; x++ {
piece = mino.NewPiece(minos[i], mino.Point{x * 5, (y * 5)})
i++
if i == len(minos) {
i = 0
}
if addToRight {
titlePiecesR = append(titlePiecesR, piece)
} else {
titlePiecesL = append(titlePiecesL, piece)
}
addToRight = !addToRight
}
}
titleName = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false).SetDynamicColors(true)
titleL = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false).SetDynamicColors(true)
titleR = tview.NewTextView().
SetScrollable(false).
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false).SetDynamicColors(true)
go handleTitle()
buttonA = tview.NewButton("A")
buttonLabelA = tview.NewTextView().SetTextAlign(tview.AlignCenter)
buttonB = tview.NewButton("B")
buttonLabelB = tview.NewTextView().SetTextAlign(tview.AlignCenter)
buttonC = tview.NewButton("C")
buttonLabelC = tview.NewTextView().SetTextAlign(tview.AlignCenter)
titleNameGrid := tview.NewGrid().SetRows(5).
AddItem(titleName, 0, 0, 1, 1, 0, 0, false).
AddItem(tview.NewTextView().SetText(SubTitle+game.Version), 1, 0, 1, 1, 0, 0, false)
titleGrid = tview.NewGrid().
SetRows(7, 3, 3, 3, 3, 3, 2).
SetColumns(-1, 38, -1).
AddItem(titleL, 0, 0, 7, 1, 0, 0, false).
AddItem(titleNameGrid, 0, 1, 1, 1, 0, 0, false).
AddItem(titleR, 0, 2, 7, 1, 0, 0, false).
AddItem(buttonA, 1, 1, 1, 1, 0, 0, false).
AddItem(buttonLabelA, 2, 1, 1, 1, 0, 0, false).
AddItem(buttonB, 3, 1, 1, 1, 0, 0, false).
AddItem(buttonLabelB, 4, 1, 1, 1, 0, 0, false).
AddItem(buttonC, 5, 1, 1, 1, 0, 0, false).
AddItem(buttonLabelC, 6, 1, 1, 1, 0, 0, false)
gameListView = tview.NewTextView().SetDynamicColors(true)
gameListButtonsGrid := tview.NewGrid().
SetColumns(-1, 1, -1, 1, -1).
AddItem(buttonA, 0, 0, 1, 1, 0, 0, false).
AddItem(pad, 0, 1, 1, 1, 0, 0, false).
AddItem(buttonB, 0, 2, 1, 1, 0, 0, false).
AddItem(pad, 0, 3, 1, 1, 0, 0, false).
AddItem(buttonC, 0, 4, 1, 1, 0, 0, false)
gameListHeader = tview.NewTextView().SetTextAlign(tview.AlignCenter)
gameListGrid = tview.NewGrid().
SetRows(7, 1, -1, 1, 3).
SetColumns(-1, 38, -1).
AddItem(titleL, 0, 0, 5, 1, 0, 0, false).
AddItem(titleNameGrid, 0, 1, 1, 1, 0, 0, false).
AddItem(titleR, 0, 2, 5, 1, 0, 0, false).
AddItem(gameListHeader, 1, 1, 1, 1, 0, 0, true).
AddItem(gameListView, 2, 1, 1, 1, 0, 0, true).
AddItem(gameListButtonsGrid, 3, 1, 1, 1, 0, 0, true).
AddItem(tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("\nRefresh: R\nPrevious: Shift+Tab - Next: Tab"), 4, 1, 1, 1, 0, 0, true)
buttonCancel = tview.NewButton("Cancel")
buttonStart = tview.NewButton("Start")
newGameSubmitGrid := tview.NewGrid().
SetColumns(-1, 10, 1, 10, -1).
AddItem(pad, 0, 0, 1, 1, 0, 0, false).
AddItem(buttonCancel, 0, 1, 1, 1, 0, 0, false).
AddItem(pad, 0, 2, 1, 1, 0, 0, false).
AddItem(buttonStart, 0, 3, 1, 1, 0, 0, false).
AddItem(pad, 0, 4, 1, 1, 0, 0, false)
newGameNameInput = tview.NewInputField().SetText("netris")
newGameMaxPlayersInput = tview.NewInputField().SetFieldWidth(3).SetAcceptanceFunc(func(textToCheck string, lastChar rune) bool {
return unicode.IsDigit(lastChar) && len(textToCheck) <= 3
})
newGameSpeedLimitInput = tview.NewInputField().SetFieldWidth(3).SetAcceptanceFunc(func(textToCheck string, lastChar rune) bool {
return unicode.IsDigit(lastChar) && len(textToCheck) <= 3
})
resetNewGameInputs()
newGameNameGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Name"), 0, 0, 1, 1, 0, 0, false).
AddItem(newGameNameInput, 0, 1, 1, 1, 0, 0, false)
newGameMaxPlayersGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Player Limit"), 0, 0, 1, 1, 0, 0, false).
AddItem(newGameMaxPlayersInput, 0, 1, 1, 1, 0, 0, false)
newGameSpeedLimitGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Speed Limit"), 0, 0, 1, 1, 0, 0, false).
AddItem(newGameSpeedLimitInput, 0, 1, 1, 1, 0, 0, false)
newGameHeader := tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("New Game")
newGameGrid = tview.NewGrid().
SetRows(7, 2, 1, 1, 1, 1, 1, 1, 1, -1, 3).
SetColumns(-1, 38, -1).
AddItem(titleL, 0, 0, 11, 1, 0, 0, false).
AddItem(titleNameGrid, 0, 1, 1, 1, 0, 0, false).
AddItem(titleR, 0, 2, 11, 1, 0, 0, false).
AddItem(newGameHeader, 1, 1, 1, 1, 0, 0, false).
AddItem(newGameNameGrid, 2, 1, 1, 1, 0, 0, false).
AddItem(pad, 3, 1, 1, 1, 0, 0, false).
AddItem(newGameMaxPlayersGrid, 4, 1, 1, 1, 0, 0, false).
AddItem(pad, 5, 1, 1, 1, 0, 0, false).
AddItem(newGameSpeedLimitGrid, 6, 1, 1, 1, 0, 0, false).
AddItem(pad, 7, 1, 1, 1, 0, 0, false).
AddItem(newGameSubmitGrid, 8, 1, 1, 1, 0, 0, false).
AddItem(pad, 9, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("\nLimits set to zero are disabled\nPrevious: Shift+Tab - Next: Tab"), 10, 1, 1, 1, 0, 0, false)
playerSettingsTitle := tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("Player Settings")
playerSettingsForm = tview.NewForm().SetButtonsAlign(tview.AlignCenter)
playerSettingsGrid = tview.NewGrid().
SetRows(7, 2, -1, 1).
SetColumns(-1, 38, -1).
AddItem(titleL, 0, 0, 4, 1, 0, 0, false).
AddItem(titleNameGrid, 0, 1, 1, 1, 0, 0, false).
AddItem(titleR, 0, 2, 4, 1, 0, 0, false).
AddItem(playerSettingsTitle, 1, 1, 1, 1, 0, 0, true).
AddItem(playerSettingsForm, 2, 1, 1, 1, 0, 0, true).
AddItem(tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("Previous: Shift+Tab - Next: Tab"), 3, 1, 1, 1, 0, 0, true)
gameSettingsTitle := tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("Game Settings")
buttonKeybindRotateCCW = tview.NewButton("Set")
buttonKeybindRotateCW = tview.NewButton("Set")
buttonKeybindMoveLeft = tview.NewButton("Set")
buttonKeybindMoveRight = tview.NewButton("Set")
buttonKeybindSoftDrop = tview.NewButton("Set")
buttonKeybindHardDrop = tview.NewButton("Set")
buttonKeybindCancel = tview.NewButton("Cancel")
buttonKeybindSave = tview.NewButton("Save")
rotateCCWGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Rotate CCW"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindRotateCCW, 0, 1, 1, 1, 0, 0, false)
rotateCWGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Rotate CW"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindRotateCW, 0, 1, 1, 1, 0, 0, false)
moveLeftGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Move Left"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindMoveLeft, 0, 1, 1, 1, 0, 0, false)
moveRightGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Move Right"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindMoveRight, 0, 1, 1, 1, 0, 0, false)
softDropGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Soft Drop"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindSoftDrop, 0, 1, 1, 1, 0, 0, false)
hardDropGrid := tview.NewGrid().
AddItem(tview.NewTextView().SetText("Hard Drop"), 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindHardDrop, 0, 1, 1, 1, 0, 0, false)
gameSettingsSubmitGrid := tview.NewGrid().
SetColumns(-1, 10, 1, 10, -1).
AddItem(pad, 0, 0, 1, 1, 0, 0, false).
AddItem(buttonKeybindCancel, 0, 1, 1, 1, 0, 0, false).
AddItem(pad, 0, 2, 1, 1, 0, 0, false).
AddItem(buttonKeybindSave, 0, 3, 1, 1, 0, 0, false).
AddItem(pad, 0, 4, 1, 1, 0, 0, false)
gameSettingsGrid = tview.NewGrid().
SetRows(7, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1).
SetColumns(-1, 38, -1).
AddItem(titleL, 0, 0, 16, 1, 0, 0, false).
AddItem(titleNameGrid, 0, 1, 1, 1, 0, 0, false).
AddItem(titleR, 0, 2, 16, 1, 0, 0, false).
AddItem(gameSettingsTitle, 1, 1, 1, 1, 0, 0, false).
AddItem(rotateCCWGrid, 2, 1, 1, 1, 0, 0, false).
AddItem(pad, 3, 1, 1, 1, 0, 0, false).
AddItem(rotateCWGrid, 4, 1, 1, 1, 0, 0, false).
AddItem(pad, 5, 1, 1, 1, 0, 0, false).
AddItem(moveLeftGrid, 6, 1, 1, 1, 0, 0, false).
AddItem(pad, 7, 1, 1, 1, 0, 0, false).
AddItem(moveRightGrid, 8, 1, 1, 1, 0, 0, false).
AddItem(pad, 9, 1, 1, 1, 0, 0, false).
AddItem(softDropGrid, 10, 1, 1, 1, 0, 0, false).
AddItem(pad, 11, 1, 1, 1, 0, 0, false).
AddItem(hardDropGrid, 12, 1, 1, 1, 0, 0, false).
AddItem(pad, 13, 1, 1, 1, 0, 0, false).
AddItem(gameSettingsSubmitGrid, 14, 1, 1, 1, 0, 0, false).
AddItem(tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetWrap(false).
SetWordWrap(false).SetText("\nPrevious: Shift+Tab - Next: Tab"), 15, 1, 1, 1, 0, 0, false)
titleContainerGrid = tview.NewGrid().SetColumns(-1, 80, -1).SetRows(-1, 24, -1).
AddItem(pad, 0, 0, 1, 3, 0, 0, false).
AddItem(pad, 1, 0, 1, 1, 0, 0, false).
AddItem(titleGrid, 1, 1, 1, 1, 0, 0, true).
AddItem(pad, 1, 2, 1, 1, 0, 0, false).
AddItem(pad, 0, 0, 1, 3, 0, 0, false)
gameListContainerGrid = tview.NewGrid().SetColumns(-1, 80, -1).SetRows(-1, 24, -1).
AddItem(pad, 0, 0, 1, 3, 0, 0, false).
AddItem(pad, 1, 0, 1, 1, 0, 0, false).
AddItem(gameListGrid, 1, 1, 1, 1, 0, 0, true).
AddItem(pad, 1, 2, 1, 1, 0, 0, false).
AddItem(pad, 0, 0, 1, 3, 0, 0, false)
newGameContainerGrid = tview.NewGrid().SetColumns(-1, 80, -1).SetRows(-1, 24, -1).
AddItem(pad, 0, 0, 1, 3, 0, 0, false).
AddItem(pad, 1, 0, 1, 1, 0, 0, false).
AddItem(newGameGrid, 1, 1, 1, 1, 0, 0, false).
AddItem(pad, 1, 2, 1, 1, 0, 0, false).
AddItem(pad, 0, 0, 1, 3, 0, 0, false)
playerSettingsContainerGrid = tview.NewGrid().SetColumns(-1, 80, -1).SetRows(-1, 24, -1).
AddItem(pad, 0, 0, 1, 3, 0, 0, false).
AddItem(pad, 1, 0, 1, 1, 0, 0, false).
AddItem(playerSettingsGrid, 1, 1, 1, 1, 0, 0, true).
AddItem(pad, 1, 2, 1, 1, 0, 0, false).
AddItem(pad, 0, 0, 1, 3, 0, 0, false)
gameSettingsContainerGrid = tview.NewGrid().SetColumns(-1, 80, -1).SetRows(-1, 24, -1).
AddItem(pad, 0, 0, 1, 3, 0, 0, false).
AddItem(pad, 1, 0, 1, 1, 0, 0, false).
AddItem(gameSettingsGrid, 1, 1, 1, 1, 0, 0, false).
AddItem(pad, 1, 2, 1, 1, 0, 0, false).
AddItem(pad, 0, 0, 1, 3, 0, 0, false)
app = app.SetInputCapture(handleKeypress)
if !skipTitle {
app.SetRoot(titleContainerGrid, true)
updateTitle()
} else {
app.SetRoot(gameGrid, true)
app.SetFocus(nil)
}
go handleDraw()
return app, nil
}
func newTitleMatrixSide() *mino.Matrix {
ev := make(chan interface{})
go func() {
for range ev {
}
}()
draw := make(chan event.DrawObject)
go func() {
for range draw {
}
}()
m := mino.NewMatrix(21, 24, 0, 1, ev, draw, mino.MatrixCustom)
return m
}
func newTitleMatrixName() *mino.Matrix {
ev := make(chan interface{})
go func() {
for range ev {
}
}()
draw := make(chan event.DrawObject)
go func() {
for range draw {
}
}()
m := mino.NewMatrix(36, 7, 0, 1, ev, draw, mino.MatrixCustom)
centerStart := (m.W / 2) - 17
var titleBlocks = []struct {
mino.Point
mino.Block
}{
// N
{mino.Point{0, 0}, mino.BlockSolidRed},
{mino.Point{0, 1}, mino.BlockSolidRed},
{mino.Point{0, 2}, mino.BlockSolidRed},
{mino.Point{0, 3}, mino.BlockSolidRed},
{mino.Point{0, 4}, mino.BlockSolidRed},
{mino.Point{1, 3}, mino.BlockSolidRed},
{mino.Point{2, 2}, mino.BlockSolidRed},
{mino.Point{3, 1}, mino.BlockSolidRed},
{mino.Point{4, 0}, mino.BlockSolidRed},
{mino.Point{4, 1}, mino.BlockSolidRed},
{mino.Point{4, 2}, mino.BlockSolidRed},
{mino.Point{4, 3}, mino.BlockSolidRed},
{mino.Point{4, 4}, mino.BlockSolidRed},
// E
{mino.Point{7, 0}, mino.BlockSolidYellow},
{mino.Point{7, 1}, mino.BlockSolidYellow},
{mino.Point{7, 2}, mino.BlockSolidYellow},
{mino.Point{7, 3}, mino.BlockSolidYellow},
{mino.Point{7, 4}, mino.BlockSolidYellow},
{mino.Point{8, 0}, mino.BlockSolidYellow},
{mino.Point{9, 0}, mino.BlockSolidYellow},
{mino.Point{8, 2}, mino.BlockSolidYellow},
{mino.Point{9, 2}, mino.BlockSolidYellow},
{mino.Point{8, 4}, mino.BlockSolidYellow},
{mino.Point{9, 4}, mino.BlockSolidYellow},
// T
{mino.Point{12, 4}, mino.BlockSolidGreen},
{mino.Point{13, 4}, mino.BlockSolidGreen},
{mino.Point{14, 0}, mino.BlockSolidGreen},
{mino.Point{14, 1}, mino.BlockSolidGreen},
{mino.Point{14, 2}, mino.BlockSolidGreen},
{mino.Point{14, 3}, mino.BlockSolidGreen},
{mino.Point{14, 4}, mino.BlockSolidGreen},
{mino.Point{15, 4}, mino.BlockSolidGreen},
{mino.Point{16, 4}, mino.BlockSolidGreen},
// R
{mino.Point{19, 0}, mino.BlockSolidCyan},
{mino.Point{19, 1}, mino.BlockSolidCyan},
{mino.Point{19, 2}, mino.BlockSolidCyan},
{mino.Point{19, 3}, mino.BlockSolidCyan},
{mino.Point{19, 4}, mino.BlockSolidCyan},
{mino.Point{20, 2}, mino.BlockSolidCyan},
{mino.Point{20, 4}, mino.BlockSolidCyan},
{mino.Point{21, 2}, mino.BlockSolidCyan},
{mino.Point{21, 4}, mino.BlockSolidCyan},
{mino.Point{22, 0}, mino.BlockSolidCyan},
{mino.Point{22, 1}, mino.BlockSolidCyan},
{mino.Point{22, 3}, mino.BlockSolidCyan},
// I
{mino.Point{25, 0}, mino.BlockSolidBlue},
{mino.Point{25, 1}, mino.BlockSolidBlue},
{mino.Point{25, 2}, mino.BlockSolidBlue},
{mino.Point{25, 3}, mino.BlockSolidBlue},
{mino.Point{25, 4}, mino.BlockSolidBlue},
// S
{mino.Point{28, 0}, mino.BlockSolidMagenta},
{mino.Point{29, 0}, mino.BlockSolidMagenta},
{mino.Point{30, 0}, mino.BlockSolidMagenta},
{mino.Point{31, 1}, mino.BlockSolidMagenta},
{mino.Point{29, 2}, mino.BlockSolidMagenta},
{mino.Point{30, 2}, mino.BlockSolidMagenta},
{mino.Point{28, 3}, mino.BlockSolidMagenta},
{mino.Point{29, 4}, mino.BlockSolidMagenta},
{mino.Point{30, 4}, mino.BlockSolidMagenta},
{mino.Point{31, 4}, mino.BlockSolidMagenta},
}
for _, titleBlock := range titleBlocks {
if !m.SetBlock(centerStart+titleBlock.X, titleBlock.Y, titleBlock.Block, false) {
log.Fatalf("failed to set title block %s", titleBlock.Point)
}
}
return m
}

View File

@ -60,6 +60,7 @@ func scrollMessages(direction int) {
draw <- event.DrawAll
}
// Render functions called here don't need to be queued (Draw is called when nil is returned)
func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
k := ev.Key()
r := ev.Rune()
@ -111,7 +112,19 @@ func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
if titleScreen > 1 {
switch k {
case tcell.KeyEscape:
titleScreen = 1
if titleScreen == 5 {
titleScreen = 4
gameListSelected = 0
titleSelectedButton = 0
app.SetRoot(gameListContainerGrid, true)
renderGameList()
updateTitle()
return nil
} else if titleScreen == 4 {
titleScreen = 0
} else {
titleScreen = 1
}
titleSelectedButton = 0
app.SetRoot(titleContainerGrid, true)
@ -157,6 +170,106 @@ func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
app.SetRoot(modal, true)
capturingKeybind = true
return nil
}
} else if titleScreen == 4 {
switch k {
case tcell.KeyUp:
if titleSelectedButton == 0 {
if gameListSelected > 0 {
gameListSelected--
}
renderGameList()
}
return nil
case tcell.KeyBacktab:
previousTitleButton()
updateTitle()
renderGameList()
return nil
case tcell.KeyDown:
if titleSelectedButton == 0 {
if gameListSelected < len(gameList)-1 {
gameListSelected++
}
renderGameList()
}
return nil
case tcell.KeyTab:
nextTitleButton()
updateTitle()
renderGameList()
return nil
case tcell.KeyEnter:
if titleSelectedButton == 0 {
if gameListSelected >= 0 && gameListSelected < len(gameList) {
joinGame <- gameList[gameListSelected].ID
}
} else if titleSelectedButton == 1 {
titleScreen = 5
titleSelectedButton = 0
resetNewGameInputs()
app.SetRoot(newGameContainerGrid, true).SetFocus(nil)
updateTitle()
} else if titleSelectedButton == 2 {
titleScreen = 5
titleSelectedButton = 0
modal := tview.NewModal().SetText("Joining another server by IP via GUI is not yet implemented.\nPlease re-launch netris with the --connect argument instead.\n\nPress Escape to go back").ClearButtons()
app.SetRoot(modal, true)
} else if titleSelectedButton == 3 {
titleScreen = 0
titleSelectedButton = 0
app.SetRoot(titleContainerGrid, true)
updateTitle()
}
return nil
default:
if titleSelectedButton == 0 {
switch r {
case 'j', 'J':
if gameListSelected < len(gameList)-1 {
gameListSelected++
}
renderGameList()
return nil
case 'k', 'K':
if gameListSelected > 0 {
gameListSelected--
}
renderGameList()
return nil
case 'r', 'R':
refreshGameList()
return nil
}
}
}
} else if titleScreen == 5 {
switch k {
case tcell.KeyBacktab:
previousTitleButton()
updateTitle()
return nil
case tcell.KeyTab:
nextTitleButton()
updateTitle()
return nil
case tcell.KeyEnter:
if titleSelectedButton == 3 {
titleScreen = 4
gameListSelected = 0
titleSelectedButton = 0
app.SetRoot(gameListContainerGrid, true)
renderGameList()
updateTitle()
} else if titleSelectedButton == 4 {
joinGame <- event.GameIDNewCustom
}
return nil
}
}
@ -174,9 +287,7 @@ func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
titleScreen = 2
titleSelectedButton = 0
app.SetRoot(playerSettingsContainerGrid, true)
app.SetFocus(playerSettingsForm)
app.Draw()
app.SetRoot(playerSettingsContainerGrid, true).SetFocus(playerSettingsForm)
return nil
case 1:
titleScreen = 3
@ -186,9 +297,7 @@ func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
draftKeybindings = make([]*Keybinding, len(keybindings))
copy(draftKeybindings, keybindings)
app.SetRoot(gameSettingsContainerGrid, true)
app.SetFocus(buttonKeybindRotateCCW)
app.Draw()
app.SetRoot(gameSettingsContainerGrid, true).SetFocus(buttonKeybindRotateCCW)
return nil
case 2:
titleScreen = 0
@ -216,10 +325,18 @@ func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
} else {
switch titleSelectedButton {
case 0:
selectMode <- event.ModePlayOnline
titleScreen = 4
titleSelectedButton = 0
gameListSelected = 0
refreshGameList()
renderGameList()
app.SetRoot(gameListContainerGrid, true).SetFocus(nil)
updateTitle()
return nil
case 1:
selectMode <- event.ModePractice
joinGame <- event.GameIDNewLocal
return nil
case 2:
titleScreen = 1

View File

@ -1,11 +1,11 @@
package main
import (
"log"
"fmt"
"math/rand"
"strconv"
"time"
"git.sr.ht/~tslocum/netris/pkg/event"
"git.sr.ht/~tslocum/netris/pkg/game"
"git.sr.ht/~tslocum/netris/pkg/mino"
"github.com/tslocum/tview"
@ -25,10 +25,24 @@ var (
titleGrid *tview.Grid
titleContainerGrid *tview.Grid
gameListSelected int
newGameGrid *tview.Grid
newGameNameInput *tview.InputField
newGameMaxPlayersInput *tview.InputField
newGameSpeedLimitInput *tview.InputField
playerSettingsForm *tview.Form
playerSettingsGrid *tview.Grid
playerSettingsContainerGrid *tview.Grid
gameList []*game.ListedGame
gameListHeader *tview.TextView
gameListView *tview.TextView
gameListGrid *tview.Grid
gameListContainerGrid *tview.Grid
newGameContainerGrid *tview.Grid
gameSettingsGrid *tview.Grid
gameSettingsContainerGrid *tview.Grid
gameGrid *tview.Grid
@ -61,7 +75,13 @@ func previousTitleButton() {
}
func nextTitleButton() {
if titleSelectedButton == 2 {
maxButton := 2
if titleScreen == 4 {
maxButton = 3
} else if titleScreen == 5 {
maxButton = 4
}
if titleSelectedButton >= maxButton {
return
}
@ -122,6 +142,12 @@ func updateTitle() {
buttonC.SetLabel("Return")
buttonLabelC.SetText("\nReturn to the last screen")
} else if titleScreen == 4 {
buttonA.SetLabel("New Game")
buttonB.SetLabel("Join by IP")
buttonC.SetLabel("Return")
} else {
if joinedGame {
buttonA.SetLabel("Resume")
@ -144,7 +170,35 @@ func updateTitle() {
}
}
if titleScreen > 1 {
if titleScreen == 4 {
switch titleSelectedButton {
case 2:
app.SetFocus(buttonB)
case 3:
app.SetFocus(buttonC)
case 1:
app.SetFocus(buttonA)
default:
app.SetFocus(nil)
}
return
} else if titleScreen == 5 {
switch titleSelectedButton {
case 1:
app.SetFocus(newGameMaxPlayersInput)
case 2:
app.SetFocus(newGameSpeedLimitInput)
case 3:
app.SetFocus(buttonCancel)
case 4:
app.SetFocus(buttonStart)
default:
app.SetFocus(newGameNameInput)
}
return
} else if titleScreen > 1 {
return
}
@ -261,123 +315,129 @@ func renderTitle() {
renderLock.Unlock()
}
func newTitleMatrixSide() *mino.Matrix {
ev := make(chan interface{})
go func() {
for range ev {
func renderGameList() {
w := 36
gameListView.Clear()
gameListView.Write(renderULCorner)
for i := 0; i < w; i++ {
gameListView.Write(renderHLine)
}
gameListView.Write(renderURCorner)
gameListView.Write([]byte("\n"))
gameListView.Write(renderVLine)
gameListView.Write([]byte(fmt.Sprintf("%-29s%s", "Game", "Players")))
gameListView.Write(renderVLine)
gameListView.Write([]byte("\n"))
gameListView.Write(renderLTee)
for i := 0; i < w; i++ {
gameListView.Write(renderHLine)
}
gameListView.Write(renderRTee)
gameListView.Write([]byte("\n"))
h := 8
for i, g := range gameList {
p := strconv.Itoa(g.Players)
if g.MaxPlayers > 0 {
p += "/" + strconv.Itoa(g.MaxPlayers)
}
}()
draw := make(chan event.DrawObject)
go func() {
for range draw {
gameListView.Write(renderVLine)
if titleSelectedButton == 0 && gameListSelected == i {
gameListView.Write([]byte("[#000000:#FFFFFF]"))
}
}()
m := mino.NewMatrix(21, 24, 0, 1, ev, draw, mino.MatrixCustom)
return m
}
func newTitleMatrixName() *mino.Matrix {
ev := make(chan interface{})
go func() {
for range ev {
gameListView.Write([]byte(fmt.Sprintf("%-29s%7s", g.Name, p)))
if titleSelectedButton == 0 && gameListSelected == i {
gameListView.Write([]byte("[-:-]"))
}
}()
gameListView.Write(renderVLine)
gameListView.Write([]byte("\n"))
draw := make(chan event.DrawObject)
go func() {
for range draw {
}
}()
m := mino.NewMatrix(36, 7, 0, 1, ev, draw, mino.MatrixCustom)
centerStart := (m.W / 2) - 17
var titleBlocks = []struct {
mino.Point
mino.Block
}{
// N
{mino.Point{0, 0}, mino.BlockSolidRed},
{mino.Point{0, 1}, mino.BlockSolidRed},
{mino.Point{0, 2}, mino.BlockSolidRed},
{mino.Point{0, 3}, mino.BlockSolidRed},
{mino.Point{0, 4}, mino.BlockSolidRed},
{mino.Point{1, 3}, mino.BlockSolidRed},
{mino.Point{2, 2}, mino.BlockSolidRed},
{mino.Point{3, 1}, mino.BlockSolidRed},
{mino.Point{4, 0}, mino.BlockSolidRed},
{mino.Point{4, 1}, mino.BlockSolidRed},
{mino.Point{4, 2}, mino.BlockSolidRed},
{mino.Point{4, 3}, mino.BlockSolidRed},
{mino.Point{4, 4}, mino.BlockSolidRed},
// E
{mino.Point{7, 0}, mino.BlockSolidYellow},
{mino.Point{7, 1}, mino.BlockSolidYellow},
{mino.Point{7, 2}, mino.BlockSolidYellow},
{mino.Point{7, 3}, mino.BlockSolidYellow},
{mino.Point{7, 4}, mino.BlockSolidYellow},
{mino.Point{8, 0}, mino.BlockSolidYellow},
{mino.Point{9, 0}, mino.BlockSolidYellow},
{mino.Point{8, 2}, mino.BlockSolidYellow},
{mino.Point{9, 2}, mino.BlockSolidYellow},
{mino.Point{8, 4}, mino.BlockSolidYellow},
{mino.Point{9, 4}, mino.BlockSolidYellow},
// T
{mino.Point{12, 4}, mino.BlockSolidGreen},
{mino.Point{13, 4}, mino.BlockSolidGreen},
{mino.Point{14, 0}, mino.BlockSolidGreen},
{mino.Point{14, 1}, mino.BlockSolidGreen},
{mino.Point{14, 2}, mino.BlockSolidGreen},
{mino.Point{14, 3}, mino.BlockSolidGreen},
{mino.Point{14, 4}, mino.BlockSolidGreen},
{mino.Point{15, 4}, mino.BlockSolidGreen},
{mino.Point{16, 4}, mino.BlockSolidGreen},
// R
{mino.Point{19, 0}, mino.BlockSolidCyan},
{mino.Point{19, 1}, mino.BlockSolidCyan},
{mino.Point{19, 2}, mino.BlockSolidCyan},
{mino.Point{19, 3}, mino.BlockSolidCyan},
{mino.Point{19, 4}, mino.BlockSolidCyan},
{mino.Point{20, 2}, mino.BlockSolidCyan},
{mino.Point{20, 4}, mino.BlockSolidCyan},
{mino.Point{21, 2}, mino.BlockSolidCyan},
{mino.Point{21, 4}, mino.BlockSolidCyan},
{mino.Point{22, 0}, mino.BlockSolidCyan},
{mino.Point{22, 1}, mino.BlockSolidCyan},
{mino.Point{22, 3}, mino.BlockSolidCyan},
// I
{mino.Point{25, 0}, mino.BlockSolidBlue},
{mino.Point{25, 1}, mino.BlockSolidBlue},
{mino.Point{25, 2}, mino.BlockSolidBlue},
{mino.Point{25, 3}, mino.BlockSolidBlue},
{mino.Point{25, 4}, mino.BlockSolidBlue},
// S
{mino.Point{28, 0}, mino.BlockSolidMagenta},
{mino.Point{29, 0}, mino.BlockSolidMagenta},
{mino.Point{30, 0}, mino.BlockSolidMagenta},
{mino.Point{31, 1}, mino.BlockSolidMagenta},
{mino.Point{29, 2}, mino.BlockSolidMagenta},
{mino.Point{30, 2}, mino.BlockSolidMagenta},
{mino.Point{28, 3}, mino.BlockSolidMagenta},
{mino.Point{29, 4}, mino.BlockSolidMagenta},
{mino.Point{30, 4}, mino.BlockSolidMagenta},
{mino.Point{31, 4}, mino.BlockSolidMagenta},
h--
}
for _, titleBlock := range titleBlocks {
if !m.SetBlock(centerStart+titleBlock.X, titleBlock.Y, titleBlock.Block, false) {
log.Fatalf("failed to set title block %s", titleBlock.Point)
if h > 0 {
for i := 0; i < h; i++ {
gameListView.Write(renderVLine)
for i := 0; i < w; i++ {
gameListView.Write([]byte(" "))
}
gameListView.Write(renderVLine)
}
}
return m
gameListView.Write(renderLLCorner)
for i := 0; i < w; i++ {
gameListView.Write(renderHLine)
}
gameListView.Write(renderLRCorner)
}
func refreshGameList() {
app.QueueUpdateDraw(func() {
gameListHeader.SetText("Finding games...")
})
go func() {
ok := fetchGameList()
app.QueueUpdateDraw(func() {
if !ok {
gameListHeader.SetText("Failed to connect to game server")
return
}
var plural string
if len(gameList) != 1 {
plural = "s"
}
gameListHeader.SetText(fmt.Sprintf("Found %d game%s", len(gameList), plural))
})
}()
}
func fetchGameList() bool {
s, err := game.Connect(connectAddress)
if err != nil {
return false
}
s.Write(&game.GameCommandListGames{})
t := time.NewTimer(10 * time.Second)
for {
select {
case <-t.C:
return false
case e := <-s.In:
if e.Command() == game.CommandListGames {
if p, ok := e.(*game.GameCommandListGames); ok {
gameList = p.Games
if gameListSelected >= len(gameList) {
gameListSelected = len(gameList) - 1
}
app.QueueUpdateDraw(renderGameList)
s.Close()
if !t.Stop() {
<-t.C
}
return true
}
}
}
}
}
func resetNewGameInputs() {
newGameNameInput.SetText("netris")
newGameMaxPlayersInput.SetText("0")
newGameSpeedLimitInput.SetText("0")
}

View File

@ -136,9 +136,11 @@ func main() {
done <- true
}()
// Connect automatically when an address or path is supplied
// TODO Connect automatically when an address or path is supplied
if connectAddress != "" {
selectMode <- event.ModePlayOnline
serverAddress = connectAddress
} else {
connectAddress = serverAddress
}
var (
@ -161,25 +163,50 @@ func main() {
}(server)
for {
mode := <-selectMode
switch mode {
case event.ModePlayOnline:
gameID := <-joinGame
if server != nil {
server.StopListening()
server = nil
}
if localListenDir != "" {
os.RemoveAll(localListenDir)
localListenDir = ""
}
if gameID == event.GameIDNewCustom || gameID >= 0 {
joinedGame = true
setTitleVisible(false)
if connectAddress == "" {
connectAddress = serverAddress
}
connectNetwork, _ := game.NetworkAndAddress(connectAddress)
if connectNetwork != "unix" {
logMessage(fmt.Sprintf("* Connecting to %s...", connectAddress))
}
s := game.Connect(connectAddress)
s, err := game.Connect(connectAddress)
if err != nil {
log.Fatal(err)
}
activeGame, err = s.JoinGame(nickname, 0, logger, draw)
var newGame *game.ListedGame
if gameID == event.GameIDNewCustom {
gameID = 0
maxPlayers, err := strconv.Atoi(newGameMaxPlayersInput.GetText())
if err != nil {
maxPlayers = 0
}
speedLimit, err := strconv.Atoi(newGameSpeedLimitInput.GetText())
if err != nil {
speedLimit = 0
}
newGame = &game.ListedGame{Name: game.GameName(newGameNameInput.GetText()), MaxPlayers: maxPlayers, SpeedLimit: speedLimit}
}
activeGame, err = s.JoinGame(nickname, gameID, newGame, logger, draw)
if err != nil {
log.Fatalf("failed to connect to %s: %s", connectAddress, err)
}
@ -189,66 +216,70 @@ func main() {
}
activeGame.LogLevel = logLevel
case event.ModePractice:
joinedGame = true
setTitleVisible(false)
continue
}
server = game.NewServer(nil)
joinedGame = true
setTitleVisible(false)
server.Logger = make(chan string, game.LogQueueSize)
if logDebug || logVerbose {
go func() {
for msg := range server.Logger {
logMessage("Local server: " + msg)
}
}()
} else {
go func() {
for range server.Logger {
}
}()
}
server = game.NewServer(nil)
localListenDir, err = ioutil.TempDir("", "netris")
if err != nil {
log.Fatal(err)
}
localListenAddress := path.Join(localListenDir, "netris.sock")
go server.Listen(localListenAddress)
localServerConn := game.Connect(localListenAddress)
activeGame, err = localServerConn.JoinGame(nickname, -1, logger, draw)
if err != nil {
panic(err)
}
activeGame.LogLevel = logLevel
if startMatrix != "" {
activeGame.Players[activeGame.LocalPlayer].Matrix.Lock()
startMatrixSplit := strings.Split(startMatrix, ",")
startMatrix = ""
var (
token int
x int
err error
)
for i := range startMatrixSplit {
token, err = strconv.Atoi(startMatrixSplit[i])
if err != nil {
panic(fmt.Sprintf("failed to parse initial matrix on token #%d", i))
}
if i%2 == 1 {
activeGame.Players[activeGame.LocalPlayer].Matrix.SetBlock(x, token, mino.BlockGarbage, false)
} else {
x = token
}
server.Logger = make(chan string, game.LogQueueSize)
if logDebug || logVerbose {
go func() {
for msg := range server.Logger {
logMessage("Local server: " + msg)
}
}()
} else {
go func() {
for range server.Logger {
}
}()
}
localListenDir, err = ioutil.TempDir("", "netris")
if err != nil {
log.Fatal(err)
}
localListenAddress := path.Join(localListenDir, "netris.sock")
go server.Listen(localListenAddress)
localServerConn, err := game.Connect(localListenAddress)
if err != nil {
log.Fatal(err)
}
activeGame, err = localServerConn.JoinGame(nickname, event.GameIDNewLocal, nil, logger, draw)
if err != nil {
panic(err)
}
activeGame.LogLevel = logLevel
if startMatrix != "" {
activeGame.Players[activeGame.LocalPlayer].Matrix.Lock()
startMatrixSplit := strings.Split(startMatrix, ",")
startMatrix = ""
var (
token int
x int
err error
)
for i := range startMatrixSplit {
token, err = strconv.Atoi(startMatrixSplit[i])
if err != nil {
panic(fmt.Sprintf("failed to parse initial matrix on token #%d", i))
}
if i%2 == 1 {
activeGame.Players[activeGame.LocalPlayer].Matrix.SetBlock(x, token, mino.BlockGarbage, false)
} else {
x = token
}
activeGame.Players[activeGame.LocalPlayer].Matrix.Unlock()
}
activeGame.Players[activeGame.LocalPlayer].Matrix.Unlock()
}
}
}

3
go.mod
View File

@ -9,7 +9,8 @@ require (
github.com/gdamore/tcell v1.3.0
github.com/gliderlabs/ssh v0.2.2
github.com/mattn/go-isatty v0.0.10
github.com/mattn/go-runewidth v0.0.5 // indirect
github.com/tslocum/tview v0.0.0-20191018041445-09b275a4b660
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2 // indirect
golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339 // indirect
)

6
go.sum
View File

@ -18,6 +18,8 @@ github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW1
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.5 h1:jrGtp51JOKTWgvLFzfG6OtZOJcK2sEnzc/U+zw7TtbA=
github.com/mattn/go-runewidth v0.0.5/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/tslocum/tview v0.0.0-20191018041445-09b275a4b660 h1:f/g7DFokEN2PRyGN4vIadgCDTTOoBoxiIxf4q1Re9PI=
@ -33,8 +35,8 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2 h1:nq114VpM8lsSlP+lyUbANecYHYiFcSNFtqcBlxRV+gA=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339 h1:zSqWKgm/o7HAnlAzBQ+aetp9fpuyytsXnKA8eiLHYQM=
golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=

View File

@ -1,9 +1,6 @@
package event
type GameMode int
const (
ModeUnknown = iota
ModePractice
ModePlayOnline
GameIDNewCustom = -2
GameIDNewLocal = -1
)

View File

@ -27,6 +27,7 @@ const (
CommandSendGarbage
CommandReceiveGarbage
CommandStats
CommandListGames
)
func (c Command) String() string {
@ -63,6 +64,8 @@ func (c Command) String() string {
return "Garbage-IN"
case CommandStats:
return "Stats"
case CommandListGames:
return "ListGames"
default:
return strconv.Itoa(int(c))
}
@ -75,7 +78,7 @@ type GameCommandInterface interface {
}
type GameCommand struct {
SourcePlayer int
SourcePlayer int `json:"sp,omitempty"`
}
func (gc *GameCommand) Source() int {
@ -96,8 +99,8 @@ func (gc *GameCommand) SetSource(source int) {
type GameCommandDisconnect struct {
GameCommand
Player int
Message string
Player int `json:"p,omitempty"`
Message string `json:"m,omitempty"`
}
func (gc GameCommandDisconnect) Command() Command {
@ -106,7 +109,7 @@ func (gc GameCommandDisconnect) Command() Command {
type GameCommandPing struct {
GameCommand
Message string
Message string `json:"m,omitempty"`
}
func (gc GameCommandPing) Command() Command {
@ -115,7 +118,7 @@ func (gc GameCommandPing) Command() Command {
type GameCommandPong struct {
GameCommand
Message string
Message string `json:"m,omitempty"`
}
func (gc GameCommandPong) Command() Command {
@ -124,8 +127,8 @@ func (gc GameCommandPong) Command() Command {
type GameCommandNickname struct {
GameCommand
Player int
Nickname string
Player int `json:"p,omitempty"`
Nickname string `json:"n,omitempty"`
}
func (gc GameCommandNickname) Command() Command {
@ -134,8 +137,8 @@ func (gc GameCommandNickname) Command() Command {
type GameCommandMessage struct {
GameCommand
Player int
Message string
Player int `json:"p,omitempty"`
Message string `json:"m,omitempty"`
}
func (gc GameCommandMessage) Command() Command {
@ -144,10 +147,12 @@ func (gc GameCommandMessage) Command() Command {
type GameCommandJoinGame struct {
GameCommand
Version int
Name string
GameID int
PlayerID int
Version int `json:"v,omitempty"`
Name string `json:"n,omitempty"`
GameID int `json:"g,omitempty"`
PlayerID int `json:"p,omitempty"`
Listing ListedGame `json:"l,omitempty"`
}
func (gc GameCommandJoinGame) Command() Command {
@ -156,7 +161,7 @@ func (gc GameCommandJoinGame) Command() Command {
type GameCommandQuitGame struct {
GameCommand
Player int
Player int `json:"p,omitempty"`
}
func (gc GameCommandQuitGame) Command() Command {
@ -165,7 +170,7 @@ func (gc GameCommandQuitGame) Command() Command {
type GameCommandUpdateGame struct {
GameCommand
Players map[int]string
Players map[int]string `json:"p,omitempty"`
}
func (gc GameCommandUpdateGame) Command() Command {
@ -174,8 +179,8 @@ func (gc GameCommandUpdateGame) Command() Command {
type GameCommandStartGame struct {
GameCommand
Seed int64
Started bool
Seed int64 `json:"s,omitempty"`
Started bool `json:"st,omitempty"`
}
func (gc GameCommandStartGame) Command() Command {
@ -184,7 +189,7 @@ func (gc GameCommandStartGame) Command() Command {
type GameCommandUpdateMatrix struct {
GameCommand
Matrixes map[int]*mino.Matrix
Matrixes map[int]*mino.Matrix `json:"m,omitempty"`
}
func (gc GameCommandUpdateMatrix) Command() Command {
@ -193,8 +198,8 @@ func (gc GameCommandUpdateMatrix) Command() Command {
type GameCommandGameOver struct {
GameCommand
Player int
Winner string
Player int `json:"p,omitempty"`
Winner string `json:"w,omitempty"`
}
func (gc GameCommandGameOver) Command() Command {
@ -203,7 +208,7 @@ func (gc GameCommandGameOver) Command() Command {
type GameCommandSendGarbage struct {
GameCommand
Lines int
Lines int `json:"l,omitempty"`
}
func (gc GameCommandSendGarbage) Command() Command {
@ -212,7 +217,7 @@ func (gc GameCommandSendGarbage) Command() Command {
type GameCommandReceiveGarbage struct {
GameCommand
Lines int
Lines int `json:"l,omitempty"`
}
func (gc GameCommandReceiveGarbage) Command() Command {
@ -221,11 +226,28 @@ func (gc GameCommandReceiveGarbage) Command() Command {
type GameCommandStats struct {
GameCommand
Created time.Time
Players int
Games int
Created time.Time `json:"c,omitempty"`
Players int `json:"p,omitempty"`
Games int `json:"g,omitempty"`
}
func (gc GameCommandStats) Command() Command {
return CommandStats
}
type ListedGame struct {
ID int
Name string `json:"n,omitempty"`
Players int `json:"p,omitempty"`
MaxPlayers int `json:"pl,omitempty"`
SpeedLimit int `json:"sl,omitempty"`
}
type GameCommandListGames struct {
GameCommand
Games []*ListedGame `json:"g,omitempty"`
}
func (gc GameCommandListGames) Command() Command {
return CommandListGames
}

View File

@ -54,7 +54,7 @@ func NewServerConn(conn net.Conn, forwardOut chan GameCommandInterface) *Conn {
return &c
}
func Connect(address string) *Conn {
func Connect(address string) (*Conn, error) {
var (
network string
conn net.Conn
@ -67,7 +67,7 @@ func Connect(address string) *Conn {
conn, err = net.DialTimeout(network, address, ConnTimeout)
if err != nil {
if tries > 25 {
log.Fatalf("failed to connect to %s: %s", address, err)
return nil, fmt.Errorf("failed to connect to %s: %s", address, err)
} else {
time.Sleep(250 * time.Millisecond)
@ -76,7 +76,7 @@ func Connect(address string) *Conn {
}
}
return NewServerConn(conn, nil)
return NewServerConn(conn, nil), nil
}
}
@ -211,7 +211,12 @@ func (s *Conn) handleRead() {
var mgc GameCommandStats
um(&mgc)
gc = &mgc
case CommandListGames:
var mgc GameCommandListGames
um(&mgc)
gc = &mgc
default:
// TODO Place beind debug log level
log.Println("unknown serverconn command", scanner.Text())
continue
}
@ -295,8 +300,16 @@ func (s *Conn) Close() {
}()
}
func (s *Conn) JoinGame(name string, gameID int, logger chan string, draw chan event.DrawObject) (*Game, error) {
s.Write(&GameCommandJoinGame{Name: name, GameID: gameID})
// When newGame is set to a ListedGame and gameID is 0, a new custom game is created
func (s *Conn) JoinGame(name string, gameID int, newGame *ListedGame, logger chan string, draw chan event.DrawObject) (*Game, error) {
joinGameCommand := GameCommandJoinGame{Name: name, GameID: gameID}
if newGame != nil {
joinGameCommand.Listing.Name = newGame.Name
joinGameCommand.Listing.MaxPlayers = newGame.MaxPlayers
joinGameCommand.Listing.SpeedLimit = newGame.SpeedLimit
}
s.Write(&joinGameCommand)
var (
g *Game
err error

View File

@ -3,7 +3,9 @@ package game
import (
"fmt"
"log"
"regexp"
"strconv"
"strings"
"sync"
"time"
@ -26,20 +28,26 @@ const (
var Version = "0.0.0"
var gameNameRegexp = regexp.MustCompile(`[^a-zA-Z0-9_\-!@#$%^&*+=,./?()\[\]{};:<>'" ]+`)
type Game struct {
ID int
ID int
Name string
Starting bool
Started bool
TimeStarted time.Time
gameOver bool
sentGameOverMatrix bool
Terminated bool
Eternal bool
Terminated bool
Local bool
LocalPlayer int
nextPlayer int
Players map[int]*Player
MaxPlayers int
Event chan interface{}
out func(GameCommandInterface)
@ -48,10 +56,12 @@ type Game struct {
logger chan string
LogLevel int
Rank int
Minos []mino.Mino
Seed int64
FallTime time.Duration
Rank int
Minos []mino.Mino
Seed int64
FallTime time.Duration
SpeedLimit int
sentPing time.Time
*sync.Mutex
@ -77,6 +87,7 @@ func NewGame(rank int, out func(GameCommandInterface), logger chan string, draw
}
g := &Game{
Name: "netris",
Rank: rank,
Minos: minos,
nextPlayer: 1,
@ -97,6 +108,8 @@ func NewGame(rank int, out func(GameCommandInterface), logger chan string, draw
g.FallTime = 850 * time.Millisecond
go g.handleDropTerminatedPlayers()
return g, nil
}
@ -333,7 +346,7 @@ func (g *Game) ResetL() {
}
func (g *Game) StopL() {
if g.Terminated {
if g.Terminated || g.Eternal {
return
}
@ -389,11 +402,6 @@ func (g *Game) handleDistributeMatrixes() {
remainingPlayers := 0
for playerID, p := range g.Players {
if p.Terminated {
g.RemovePlayerL(playerID)
continue
}
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 {
@ -759,3 +767,30 @@ func (g *Game) ProcessAction(a event.GameAction) {
}
}
}
func (g *Game) handleDropTerminatedPlayers() {
for {
time.Sleep(15 * time.Second)
if g.Terminated {
return
}
for playerID, p := range g.Players {
if p.Terminated {
g.RemovePlayerL(playerID)
}
}
}
}
func GameName(name string) string {
name = gameNameRegexp.ReplaceAllString(strings.TrimSpace(name), "")
if len(name) > 28 {
name = name[:28]
} else if name == "" {
name = "netris"
}
return name
}

View File

@ -14,7 +14,7 @@ const (
PlayerUnknown = 0
)
var nickRegexp = regexp.MustCompile(`[^a-zA-Z0-9_\-!@#$%^&*+=,./]+`)
var nickRegexp = regexp.MustCompile(`[^a-zA-Z0-9_\-!@#$%^&*+=,./?]+`)
type ConnectingPlayer struct {
Name string

View File

@ -1,9 +1,11 @@
package game
import (
"encoding/json"
"fmt"
"log"
"net"
"sort"
"strings"
"sync"
"time"
@ -51,6 +53,36 @@ func NewServer(si []ServerInterface) *Server {
s := &Server{I: si, In: in, Out: out, Games: make(map[int]*Game), created: time.Now()}
var (
g *Game
err error
)
g, err = s.NewGame()
if err != nil {
log.Fatal(err)
}
g.Eternal = true
g.Name = "No speed limit"
g, err = s.NewGame()
if err != nil {
log.Fatal(err)
}
g.Eternal = true
g.Name = "Speed limit 100"
g.SpeedLimit = 100
g, err = s.NewGame()
if err != nil {
log.Fatal(err)
}
g.Eternal = true
g.Name = "Speed limit 40"
g.SpeedLimit = 40
s.NewPlayers = make(chan *IncomingPlayer, CommandQueueSize)
go s.accept()
@ -118,7 +150,7 @@ func (s *Server) removeTerminatedGames() {
}
}
func (s *Server) FindGame(p *Player, gameID int) *Game {
func (s *Server) FindGame(p *Player, gameID int, newGame ListedGame) *Game {
s.Lock()
defer s.Unlock()
@ -127,40 +159,68 @@ func (s *Server) FindGame(p *Player, gameID int) *Game {
err error
)
// 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 _, gm := range s.Games {
if gm != nil && !gm.Terminated {
g = gm
break
}
}
}
// Create a new game
if g == nil {
if newGame.Name != "" {
// Create a custom game
g, err = s.NewGame()
if err != nil {
panic(err)
}
if gameID == -1 {
g.Local = true
g.Lock()
g.Name = GameName(newGame.Name)
g.MaxPlayers = newGame.MaxPlayers
if g.MaxPlayers < 0 {
g.MaxPlayers = 0
} else if g.MaxPlayers > 999 {
g.MaxPlayers = 999
}
g.SpeedLimit = newGame.SpeedLimit
if g.SpeedLimit < 0 {
g.SpeedLimit = 0
} else if g.SpeedLimit > 999 {
g.SpeedLimit = 999
}
g.Unlock()
} else if gameID > 0 {
// Join a game by its ID
if gm, ok := s.Games[gameID]; ok && !gm.Terminated && (gm.MaxPlayers == 0 || len(gm.Players) < gm.MaxPlayers) {
g = gm
} else {
p.Write(&GameCommandMessage{Message: "Failed to join game - Player limit reached"})
return nil
}
} else if gameID == 0 {
// Join any game
for _, gm := range s.Games {
if gm != nil && !gm.Terminated && (gm.MaxPlayers == 0 || len(gm.Players) < gm.MaxPlayers) {
g = gm
break
}
}
} else {
// Create a local game
g, err = s.NewGame()
if err != nil {
panic(err)
}
g.Local = true
}
if g == nil {
p.Write(&GameCommandMessage{Message: "Failed to join game"})
return nil
}
g.Lock()
g.AddPlayerL(p)
if gameID == -1 {
if gameID == event.GameIDNewLocal {
go g.Start(0)
} else if len(g.Players) > 1 {
go s.initiateAutoStart(g)
@ -179,23 +239,61 @@ func (s *Server) accept() {
p := NewPlayer(np.Name, np.Conn)
s.Log("Incoming connection from ", np.Name)
go s.handleJoinGame(p)
go s.handleNewPlayer(p)
}
}
func (s *Server) handleJoinGame(pl *Player) {
func (s *Server) handleNewPlayer(pl *Player) {
handled := false
go func() {
time.Sleep(10 * time.Second)
if !handled {
pl.Close()
}
}()
for e := range pl.In {
if e.Command() == CommandJoinGame {
switch e.Command() {
case CommandListGames:
if _, ok := e.(*GameCommandListGames); ok {
var gl []*ListedGame
for _, g := range s.Games {
if g.Terminated {
continue
}
gl = append(gl, &ListedGame{ID: g.ID, Name: g.Name, Players: len(g.Players), MaxPlayers: g.MaxPlayers, SpeedLimit: g.SpeedLimit})
}
sort.Slice(gl, func(i, j int) bool {
if gl[i].Players == gl[j].Players {
return gl[i].Name < gl[j].Name
}
return gl[i].Players > gl[j].Players
})
pl.Write(&GameCommandListGames{Games: gl})
}
case CommandJoinGame:
if p, ok := e.(*GameCommandJoinGame); ok {
pl.Name = Nickname(p.Name)
g := s.FindGame(pl, p.GameID)
g := s.FindGame(pl, p.GameID, p.Listing)
if g == nil {
return
}
s.Logf("Adding %s to game %d", pl.Name, g.ID)
if p.Listing.Name == "" {
g.Logf(LogStandard, "Player %s joined %s", pl.Name, g.Name)
} else {
g.Logf(LogStandard, "Player %s created new game %s", pl.Name, g.Name)
}
go s.handleGameCommands(pl, g)
handled = true
return
}
}
@ -220,10 +318,19 @@ func (s *Server) initiateAutoStart(g *Game) {
}
func (s *Server) handleGameCommands(pl *Player, g *Game) {
var (
msgJSON []byte
err error
)
for e := range pl.In {
c := e.Command()
if (c != CommandPing && c != CommandPong && c != CommandUpdateMatrix) || g.LogLevel >= LogVerbose {
s.Log("REMOTE handle game command ", e.Command(), " from ", e.Source(), e)
msgJSON, err = json.Marshal(e)
if err != nil {
log.Fatal(err)
}
g.Logf(LogStandard, "%d -> %s %s", e.Source(), e.Command(), msgJSON)
}
g.Lock()
@ -232,7 +339,7 @@ func (s *Server) handleGameCommands(pl *Player, g *Game) {
case CommandMessage:
if p, ok := e.(*GameCommandMessage); ok {
if player, ok := g.Players[p.SourcePlayer]; ok {
s.Log("<" + player.Name + "> " + p.Message)
s.Logf("<%s> %s", player.Name, p.Message)
msg := strings.ReplaceAll(strings.TrimSpace(p.Message), "\n", "")
if msg != "" {
@ -258,6 +365,13 @@ func (s *Server) handleGameCommands(pl *Player, g *Game) {
if pl, ok := g.Players[p.SourcePlayer]; ok {
for _, m := range p.Matrixes {
pl.Matrix.Replace(m)
if g.SpeedLimit > 0 && m.Speed > g.SpeedLimit+5 && time.Since(g.TimeStarted) > 7*time.Second {
pl.Matrix.SetGameOver()
g.WriteMessage(fmt.Sprintf("%s went too fast and crashed", pl.Name))
g.WriteAllL(&GameCommandGameOver{Player: p.SourcePlayer})
}
}
m := pl.Matrix

View File

@ -35,26 +35,26 @@ type Matrix struct {
Bag *Bag `json:"-"`
P *Piece
PlayerName string
PlayerName string `json:"pn,omitempty"`
Type MatrixType
Type MatrixType `json:"ty,omitempty"`
Event chan<- interface{} `json:"-"`
Move chan int `json:"-"`
draw chan event.DrawObject
Combo int
Combo int `json:"mc,omitempty"`
ComboStart time.Time `json:"-"`
ComboEnd time.Time `json:"-"`
PendingGarbage int `json:"-"`
PendingGarbageTime time.Time `json:"-"`
LinesCleared int
GarbageSent int
GarbageReceived int
Speed int
LinesCleared int `json:"lc,omitempty"`
GarbageSent int `json:"gs,omitempty"`
GarbageReceived int `json:"gr,omitempty"`
Speed int `json:"sp,omitempty"`
GameOver bool
GameOver bool `json:"go,omitempty"`
lands []time.Time

View File

@ -73,11 +73,11 @@ var AllRotationOffsets = map[PieceType][]RotationOffsets{
{{0, 0}, {1, 2}, {0, 0}, {-1, 2}}}}
*/
type Piece struct {
Point
Mino
Ghost Block
Solid Block
Rotation int
Point `json:"pp,omitempty"`
Mino `json:"pm,omitempty"`
Ghost Block `json:"pg,omitempty"`
Solid Block `json:"ps,omitempty"`
Rotation int `json:"pr,omitempty"`
original Mino
pivotsCW []Point