Add title screen

This commit is contained in:
Trevor Slocum 2019-10-17 06:41:20 -07:00
parent 4d167068e8
commit 893c5b5ad1
12 changed files with 924 additions and 148 deletions

View File

@ -1,3 +1,6 @@
This document covers command-line options available when launching netris. Some
options (such as keybindings) are only available in-game.
# Client
```

View File

@ -24,16 +24,29 @@ var (
inputActive bool
showDetails bool
app *tview.Application
grid *tview.Grid
inputView *tview.InputField
mtx *tview.TextView
side *tview.TextView
buffer *tview.TextView
recent *tview.TextView
lowerPages *tview.Pages
app *tview.Application
titleGrid *tview.Grid
titleContainerGrid *tview.Grid
playerSettingsForm *tview.Form
playerSettingsGrid *tview.Grid
playerSettingsContainerGrid *tview.Grid
gameSettingsForm *tview.Form
gameSettingsGrid *tview.Grid
gameSettingsContainerGrid *tview.Grid
gameGrid *tview.Grid
titleName *tview.TextView
titleL *tview.TextView
titleR *tview.TextView
inputView *tview.InputField
mtx *tview.TextView
side *tview.TextView
buffer *tview.TextView
recent *tview.TextView
draw = make(chan event.DrawObject, game.CommandQueueSize)
joinedGame bool
draw = make(chan event.DrawObject, game.CommandQueueSize)
selectMode = make(chan event.GameMode, game.CommandQueueSize)
renderLock = new(sync.Mutex)
renderBuffer bytes.Buffer
@ -42,8 +55,15 @@ var (
screenW, screenH int
newScreenW, newScreenH int
nickname = "Anonymous"
nicknameDraft string
inputHeight, mainHeight, newLogLines int
)
const DefaultStatusText = "Press Enter to chat, Z/X to rotate, arrow keys or HJKL to move/drop"
// TODO: Darken ghost color?
var renderBlock = map[mino.Block][]byte{
mino.BlockNone: []byte(" "),
@ -77,6 +97,7 @@ func initGUI() (*tview.Application, error) {
app.SetBeforeDrawFunc(handleResize)
inputView = tview.NewInputField().
SetText(DefaultStatusText).
SetLabel("").
SetFieldWidth(0).
SetFieldBackgroundColor(tcell.ColorDefault).
@ -90,7 +111,7 @@ func initGUI() (*tview.Application, error) {
return event
})
grid = tview.NewGrid().
gameGrid = tview.NewGrid().
SetBorders(false).
SetRows(2+(20*blockSize), -1)
@ -125,57 +146,246 @@ func initGUI() (*tview.Application, error) {
SetWrap(true).
SetWordWrap(true)
recent.SetDrawFunc(func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
logMutex.Lock()
showLogLines = height
if showLogLines < 1 {
showLogLines = 1
}
logMutex.Unlock()
return recent.GetInnerRect()
})
lowerPages = tview.NewPages()
lowerPages = lowerPages.AddPage("input",
inputView,
true, false)
lowerPages = lowerPages.AddPage("recent",
recent,
true, true)
grid = grid.SetColumns(1, 4+(10*blockSize), 10, -1).
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(lowerPages, 1, 1, 1, 3, 0, 0, true)
AddItem(inputView, 1, 1, 1, 3, 0, 0, true).
AddItem(recent, 2, 1, 1, 3, 0, 0, true)
// Set up title screen
titleVisible = true
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().
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false).SetDynamicColors(true)
titleL = tview.NewTextView().
SetTextAlign(tview.AlignLeft).
SetWrap(false).
SetWordWrap(false).SetDynamicColors(true)
titleR = tview.NewTextView().
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)
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(titleName, 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("\nPlayer 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(titleName, 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("\nGame Settings")
gameSettingsForm = tview.NewForm().SetButtonsAlign(tview.AlignCenter)
gameSettingsGrid = tview.NewGrid().
SetRows(7, 2, -1, 1).
SetColumns(-1, 38, -1).
AddItem(titleL, 0, 0, 3, 1, 0, 0, false).
AddItem(titleName, 0, 1, 1, 1, 0, 0, false).
AddItem(titleR, 0, 2, 3, 1, 0, 0, false).
AddItem(gameSettingsTitle, 1, 1, 1, 1, 0, 0, true).
AddItem(gameSettingsForm, 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)
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, true).
AddItem(tview.NewTextView(), 1, 2, 1, 1, 0, 0, false).
AddItem(tview.NewTextView(), 0, 0, 1, 3, 0, 0, false)
app = app.SetInputCapture(handleKeypress)
app = app.SetRoot(grid, true)
app.SetRoot(titleContainerGrid, true)
updateTitle()
go handleDraw()
return app, nil
}
func resetPlayerSettingsForm() {
playerSettingsForm.Clear(true).AddInputField("Name", nickname, 0, nil, func(text string) {
nicknameDraft = text
}).AddButton("Cancel", func() {
titleScreen = 1
titleSelectedButton = 0
app.SetRoot(titleContainerGrid, true)
updateTitle()
}).AddButton("Save", func() {
if nicknameDraft != "" && game.Nickname(nicknameDraft) != nickname {
nickname = game.Nickname(nicknameDraft)
if activeGame != nil {
activeGame.Event <- &event.NicknameEvent{Nickname: nickname}
}
}
titleScreen = 1
titleSelectedButton = 0
app.SetRoot(titleContainerGrid, true)
updateTitle()
})
}
func resetGameSettingsForm() {
gameSettingsForm.Clear(true).
AddInputField("Custom", "", 0, nil, nil).
AddInputField("Keybindings", "", 0, nil, nil).
AddInputField("Are", "", 0, nil, nil).
AddInputField("Coming", "", 0, nil, nil).
AddInputField("Soon", "", 0, nil, nil).
AddButton("Cancel", func() {
titleScreen = 1
titleSelectedButton = 0
app.SetRoot(titleContainerGrid, true)
updateTitle()
}).AddButton("Save", func() {
if nicknameDraft != "" && game.Nickname(nicknameDraft) != nickname {
nickname = game.Nickname(nicknameDraft)
if activeGame != nil {
activeGame.Event <- &event.NicknameEvent{Nickname: nickname}
}
}
titleScreen = 1
titleSelectedButton = 0
app.SetRoot(titleContainerGrid, true)
updateTitle()
})
}
func handleResize(screen tcell.Screen) bool {
newScreenW, newScreenH = screen.Size()
if newScreenW != screenW || newScreenH != screenH {
screenW, screenH = newScreenW, newScreenH
// TODO Obey initial set blocksize or auto
if screenW >= 80 && screenH >= 44 {
blockSize = 2
} else {
blockSize = 1
if !fixedBlockSize {
if screenW >= 80 && screenH >= 44 {
blockSize = 2
} else {
blockSize = 1
}
}
multiplayerMatrixSize = (screenW - ((10 * blockSize) + 16)) / ((10 * blockSize) + 4)
grid.SetRows(2+(20*blockSize), -1).SetColumns(1, 4+(10*blockSize), 10, -1)
inputHeight = 1
mainHeight = (20 * blockSize) + 2
if screenH > mainHeight+5 {
mainHeight += 2
inputHeight++
} else if screenH > mainHeight+2 {
mainHeight++
}
newLogLines = (screenH - mainHeight) - inputHeight
if newLogLines > 0 {
showLogLines = newLogLines
} else {
showLogLines = 1
}
gameGrid.SetRows(mainHeight, inputHeight, -1).SetColumns(1, 4+(10*blockSize), 10, -1)
logMutex.Lock()
renderLogMessages = true
@ -255,12 +465,17 @@ func setInputStatus(active bool) {
inputActive = active
if active {
if inputActive {
inputView.SetText("")
lowerPages = lowerPages.SwitchToPage("input")
inputView.SetLabel("> ")
app.SetFocus(inputView)
} else {
lowerPages = lowerPages.SwitchToPage("recent")
inputView.SetText(DefaultStatusText)
inputView.SetLabel("")
app.SetFocus(nil)
}
app.Draw()
}
func setShowDetails(active bool) {
@ -395,20 +610,23 @@ func renderMatrix(m *mino.Matrix) []byte {
m.DrawPiecesL()
// Draw preview matrix at block size 2 max
bs := blockSize
if m.Preview {
if m.Type == mino.MatrixPreview {
// Draw preview matrix at block size 2 max
if bs > 2 {
bs = 2
}
if bs > 1 {
renderBuffer.WriteRune('\n')
}
} else if m.Type == mino.MatrixCustom {
bs = 1
}
for y := m.H - 1; y >= 0; y-- {
for j := 0; j < bs; j++ {
if !m.Preview {
if m.Type == mino.MatrixStandard {
renderBuffer.Write(renderVLine)
} else {
iPieceNext := m.Bag != nil && m.Bag.Next().String() == mino.TetrominoI
@ -425,15 +643,17 @@ func renderMatrix(m *mino.Matrix) []byte {
}
}
if !m.Preview {
if m.Type == mino.MatrixStandard {
renderBuffer.Write(renderVLine)
}
renderBuffer.WriteRune('\n')
if y != 0 || m.Type == mino.MatrixStandard {
renderBuffer.WriteRune('\n')
}
}
}
if m.Preview {
if m.Type != mino.MatrixStandard {
return renderBuffer.Bytes()
}
@ -503,7 +723,7 @@ func renderMatrixes(mx []*mino.Matrix) []byte {
renderBuffer.WriteString(div)
}
if !m.Preview {
if m.Type == mino.MatrixStandard {
renderBuffer.Write(renderVLine)
}
@ -513,7 +733,7 @@ func renderMatrixes(mx []*mino.Matrix) []byte {
}
}
if !m.Preview {
if m.Type == mino.MatrixStandard {
renderBuffer.Write(renderVLine)
}
}
@ -553,7 +773,7 @@ func renderMatrixes(mx []*mino.Matrix) []byte {
func logMessage(message string) {
logMutex.Lock()
logMessages = append(logMessages, message)
logMessages = append(logMessages, time.Now().Format(LogTimeFormat)+" "+message)
renderLogMessages = true
logMutex.Unlock()
}

View File

@ -9,6 +9,103 @@ func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
k := ev.Key()
r := ev.Rune()
if titleVisible {
// TODO: During keybind change, record key, rune and modifier
if titleScreen > 1 {
switch k {
case tcell.KeyEscape:
titleScreen = 1
titleSelectedButton = 0
app.SetRoot(titleContainerGrid, true)
updateTitle()
return nil
}
return ev
}
switch k {
case tcell.KeyEnter:
if titleScreen == 1 {
switch titleSelectedButton {
case 0:
resetPlayerSettingsForm()
titleScreen = 2
titleSelectedButton = 0
app.SetRoot(playerSettingsContainerGrid, true)
app.SetFocus(playerSettingsForm)
app.Draw()
case 1:
resetGameSettingsForm()
titleScreen = 3
titleSelectedButton = 0
app.SetRoot(gameSettingsContainerGrid, true)
app.SetFocus(gameSettingsForm)
app.Draw()
case 2:
titleScreen = 0
updateTitle()
}
} else {
if joinedGame {
switch titleSelectedButton {
case 0:
setTitleVisible(false)
case 1:
titleScreen = 1
titleSelectedButton = 0
updateTitle()
case 2:
done <- true
}
} else {
switch titleSelectedButton {
case 0:
selectMode <- event.ModePlayOnline
case 1:
selectMode <- event.ModePractice
case 2:
titleScreen = 1
titleSelectedButton = 0
updateTitle()
}
}
}
case tcell.KeyUp, tcell.KeyBacktab:
previousTitleButton()
case tcell.KeyDown, tcell.KeyTab:
nextTitleButton()
case tcell.KeyEscape:
if titleScreen == 1 {
titleScreen = 0
titleSelectedButton = 0
} else if joinedGame {
setTitleVisible(false)
} else {
done <- true
}
default:
switch r {
case 'k', 'K':
previousTitleButton()
case 'j', 'J':
nextTitleButton()
}
}
updateTitle()
return ev
}
if inputActive {
if k == tcell.KeyEnter {
msg := inputView.GetText()
@ -74,7 +171,7 @@ func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
case tcell.KeyTab:
setShowDetails(!showDetails)
case tcell.KeyEscape:
done <- true
setTitleVisible(true)
}
switch r {

338
cmd/netris/gui_title.go Normal file
View File

@ -0,0 +1,338 @@
package main
import (
"log"
"math/rand"
"time"
"git.sr.ht/~tslocum/netris/pkg/event"
"git.sr.ht/~tslocum/netris/pkg/game"
"git.sr.ht/~tslocum/netris/pkg/mino"
"github.com/rivo/tview"
)
var (
playerForm *tview.Form
titleVisible bool
titleScreen int
titleSelectedButton int
drawTitle = make(chan struct{}, game.CommandQueueSize)
buttonA *tview.Button
buttonB *tview.Button
buttonC *tview.Button
buttonLabelA *tview.TextView
buttonLabelB *tview.TextView
buttonLabelC *tview.TextView
titleMatrixL = newTitleMatrixSide()
titleMatrix = newTitleMatrixName()
titleMatrixR = newTitleMatrixSide()
titlePiecesL []*mino.Piece
titlePiecesR []*mino.Piece
)
func previousTitleButton() {
if titleSelectedButton == 0 {
return
}
titleSelectedButton--
}
func nextTitleButton() {
if titleSelectedButton == 2 {
return
}
titleSelectedButton++
}
func setTitleVisible(visible bool) {
if titleVisible == visible {
return
}
titleVisible = visible
if !titleVisible {
app.SetRoot(gameGrid, true)
app.SetFocus(nil)
} else {
titleScreen = 0
titleSelectedButton = 0
drawTitle <- struct{}{}
app.SetRoot(titleContainerGrid, true)
updateTitle()
}
}
func updateTitle() {
if titleScreen == 1 {
buttonA.SetLabel("Player Settings")
buttonLabelA.SetText("\nChange name")
buttonB.SetLabel("Game Settings")
buttonLabelB.SetText("\nChange keybindings")
buttonC.SetLabel("Return")
buttonLabelC.SetText("\nReturn to the last screen")
} else {
if joinedGame {
buttonA.SetLabel("Resume")
buttonLabelA.SetText("\nResume game in progress")
buttonB.SetLabel("Settings")
buttonLabelB.SetText("\nChange player name, keybindings, etc")
buttonC.SetLabel("Quit")
buttonLabelC.SetText("\nQuit game")
} else {
buttonA.SetLabel("Play")
buttonLabelA.SetText("\nPlay with others online")
buttonB.SetLabel("Practice")
buttonLabelB.SetText("\nPlay by yourself")
buttonC.SetLabel("Settings")
buttonLabelC.SetText("\nPlayer name, keybindings, etc.")
}
}
if titleScreen > 1 {
return
}
switch titleSelectedButton {
case 1:
app.SetFocus(buttonB)
case 2:
app.SetFocus(buttonC)
default:
app.SetFocus(buttonA)
}
}
func handleTitle() {
var t *time.Ticker
for {
if t == nil {
t = time.NewTicker(850 * time.Millisecond)
} else {
select {
case <-t.C:
case <-drawTitle:
if t != nil {
t.Stop()
}
t = time.NewTicker(850 * time.Millisecond)
}
}
if !titleVisible {
continue
}
titleMatrixL.ClearOverlay()
for _, p := range titlePiecesL {
p.Y -= 1
if p.Y < -3 {
p.Y = titleMatrixL.H + 2
}
if rand.Intn(4) == 0 {
p.Mino = p.Rotate(1, 0)
p.ApplyRotation(1, 0)
}
for _, m := range p.Mino {
titleMatrixL.SetBlock(p.X+m.X, p.Y+m.Y, p.Solid, true)
}
}
titleMatrixR.ClearOverlay()
for _, p := range titlePiecesR {
p.Y -= 1
if p.Y < -3 {
p.Y = titleMatrixL.H + 2
}
if rand.Intn(4) == 0 {
p.Mino = p.Rotate(1, 0)
p.ApplyRotation(1, 0)
}
for _, m := range p.Mino {
if titleMatrixR.Block(p.X+m.X, p.Y+m.Y) != mino.BlockNone {
continue
}
titleMatrixR.SetBlock(p.X+m.X, p.Y+m.Y, p.Solid, true)
}
}
app.QueueUpdateDraw(renderTitle)
}
}
func renderTitle() {
var newBlock mino.Block
for i, b := range titleMatrix.M {
switch b {
case mino.BlockSolidRed:
newBlock = mino.BlockSolidMagenta
case mino.BlockSolidYellow:
newBlock = mino.BlockSolidRed
case mino.BlockSolidGreen:
newBlock = mino.BlockSolidYellow
case mino.BlockSolidCyan:
newBlock = mino.BlockSolidGreen
case mino.BlockSolidBlue:
newBlock = mino.BlockSolidCyan
case mino.BlockSolidMagenta:
newBlock = mino.BlockSolidBlue
default:
continue
}
titleMatrix.M[i] = newBlock
}
titleName.Clear()
titleName.Write(renderMatrix(titleMatrix))
titleL.Clear()
titleL.Write(renderMatrix(titleMatrixL))
titleR.Clear()
titleR.Write(renderMatrix(titleMatrixR))
}
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)
baseStart := 1
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, baseStart+titleBlock.Y, titleBlock.Block, false) {
log.Fatalf("failed to set title block %s", titleBlock.Point)
}
}
return m
}

View File

@ -15,6 +15,8 @@ import (
"syscall"
"time"
"git.sr.ht/~tslocum/netris/pkg/event"
"git.sr.ht/~tslocum/netris/pkg/game"
"git.sr.ht/~tslocum/netris/pkg/mino"
"github.com/mattn/go-isatty"
@ -28,10 +30,12 @@ var (
connectAddress string
debugAddress string
nickname string
startMatrix string
blockSize = 0
nicknameFlag string
blockSize = 0
fixedBlockSize bool
logDebug bool
logVerbose bool
@ -39,7 +43,7 @@ var (
logMessages []string
renderLogMessages bool
logMutex = new(sync.Mutex)
showLogLines = 7 // TODO Set in resize func?
showLogLines = 7
)
const (
@ -66,7 +70,7 @@ func main() {
}()
flag.IntVar(&blockSize, "scale", 0, "UI scale")
flag.StringVar(&nickname, "nick", "Anonymous", "nickname")
flag.StringVar(&nicknameFlag, "nick", "", "nickname")
flag.StringVar(&startMatrix, "matrix", "", "pre-fill matrix with pieces")
flag.StringVar(&connectAddress, "connect", "", "connect to server address or socket path")
flag.StringVar(&debugAddress, "debug-address", "", "address to serve debug info")
@ -79,8 +83,12 @@ func main() {
log.Fatal("failed to start netris: non-interactive terminals are not supported")
}
if blockSize > 3 {
blockSize = 3
if blockSize > 0 {
fixedBlockSize = true
if blockSize > 3 {
blockSize = 3
}
}
logLevel := game.LogStandard
@ -90,6 +98,10 @@ func main() {
logLevel = game.LogDebug
}
if game.Nickname(nicknameFlag) != "" {
nickname = game.Nickname(nicknameFlag)
}
if debugAddress != "" {
go func() {
log.Fatal(http.ListenAndServe(debugAddress, nil))
@ -109,13 +121,10 @@ func main() {
done <- true
}()
inputActive = true
setInputStatus(false)
logger := make(chan string, game.LogQueueSize)
go func() {
for msg := range logger {
logMessage(time.Now().Format(LogTimeFormat) + " " + msg)
logMessage(msg)
}
}()
@ -129,79 +138,109 @@ func main() {
done <- true
}()
// Connect to a game
// Connect automatically when an address or path is supplied
if connectAddress != "" {
s := game.Connect(connectAddress)
selectMode <- event.ModePlayOnline
}
activeGame, err = s.JoinGame(nickname, 0, logger, draw)
if err != nil {
panic(err)
}
activeGame.LogLevel = logLevel
var server *game.Server
go func(server *game.Server) {
<-done
if server != nil {
server.StopListening()
}
closeGUI()
return
}
// Host a game
server := game.NewServer(nil)
os.Exit(0)
}(server)
server.Logger = make(chan string, game.LogQueueSize)
if logDebug || logVerbose {
go func() {
for msg := range server.Logger {
logMessage(time.Now().Format(LogTimeFormat) + " Local server: " + msg)
for {
mode := <-selectMode
switch mode {
case event.ModePlayOnline:
joinedGame = true
setTitleVisible(false)
if connectAddress == "" {
connectAddress = game.DefaultServer
}
}()
} else {
go func() {
for range server.Logger {
connectNetwork, _ := game.NetworkAndAddress(connectAddress)
if connectNetwork != "unix" {
logMessage(fmt.Sprintf("* Connecting to %s...", connectAddress))
draw <- event.DrawMessages
}
}()
}
localListenAddress := fmt.Sprintf("localhost:%d", game.DefaultPort)
s := game.Connect(connectAddress)
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])
activeGame, err = s.JoinGame(nickname, 0, logger, draw)
if err != nil {
panic(fmt.Sprintf("failed to parse initial matrix on token #%d", i))
log.Fatalf("failed to connect to %s: %s", connectAddress, err)
}
if i%2 == 1 {
activeGame.Players[activeGame.LocalPlayer].Matrix.SetBlock(x, token, mino.BlockGarbage, false)
if activeGame == nil {
log.Fatal("failed to connect to server")
}
activeGame.LogLevel = logLevel
case event.ModePractice:
joinedGame = true
setTitleVisible(false)
server = game.NewServer(nil)
server.Logger = make(chan string, game.LogQueueSize)
if logDebug || logVerbose {
go func() {
for msg := range server.Logger {
logMessage("Local server: " + msg)
}
}()
} else {
x = token
go func() {
for range server.Logger {
}
}()
}
localListenAddress := fmt.Sprintf("localhost:%d", game.DefaultPort)
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
}
}
activeGame.Players[activeGame.LocalPlayer].Matrix.Unlock()
}
}
activeGame.Players[activeGame.LocalPlayer].Matrix.Unlock()
}
<-done
server.StopListening()
closeGUI()
}

View File

@ -10,6 +10,11 @@ type MessageEvent struct {
Message string
}
type NicknameEvent struct {
Event
Nickname string
}
type GameOverEvent struct {
Event
}

9
pkg/event/game.go Normal file
View File

@ -0,0 +1,9 @@
package event
type GameMode int
const (
ModeUnknown = iota
ModePractice
ModePlayOnline
)

View File

@ -20,7 +20,10 @@ const (
LogVerbose
)
const DefaultPort = 1984
const (
DefaultPort = 1984
DefaultServer = "netris.rocketnine.space"
)
type Game struct {
Rank int
@ -127,10 +130,10 @@ func (g *Game) AddPlayerL(p *Player) {
g.Players[p.Player] = p
p.Preview = mino.NewMatrix(g.Rank, g.Rank-1, 0, 1, g.Event, g.draw, true)
p.Preview = mino.NewMatrix(g.Rank, g.Rank-1, 0, 1, g.Event, g.draw, mino.MatrixPreview)
p.Preview.PlayerName = p.Name
p.Matrix = mino.NewMatrix(10, 20, 20, 1, g.Event, g.draw, false)
p.Matrix = mino.NewMatrix(10, 20, 20, 1, g.Event, g.draw, mino.MatrixStandard)
p.Matrix.PlayerName = p.Name
if g.Started {
@ -415,7 +418,7 @@ func (g *Game) handleDistributeMatrixes() {
go func() {
for {
time.Sleep(5 * time.Second)
time.Sleep(7 * time.Second)
if g.Terminated {
return
} else if len(g.Players) > 1 {
@ -429,6 +432,7 @@ func (g *Game) handleDistributeMatrixes() {
matrixes = make(map[int]*mino.Matrix)
for playerID, player := range g.Players {
player.Matrix.PlayerName = player.Name
player.Matrix.GarbageSent = player.totalGarbageSent
player.Matrix.GarbageReceived = player.totalGarbageReceived
@ -466,6 +470,23 @@ func (g *Game) HandleReadCommands(in chan GameCommandInterface) {
}
g.Log(LogStandard, prefix+p.Message)
}
case CommandNickname:
if p, ok := e.(*GameCommandNickname); ok {
if player, ok := g.Players[p.Player]; ok {
newNick := Nickname(p.Nickname)
if newNick != "" && newNick != player.Name {
oldNick := player.Name
player.Name = newNick
if p.Player == g.LocalPlayer {
g.Players[g.LocalPlayer].Matrix.PlayerName = newNick
}
g.Logf(LogStandard, "* %s is now known as %s", oldNick, newNick)
}
}
}
case CommandJoinGame:
g.ResetL()
case CommandQuitGame:
@ -488,6 +509,9 @@ func (g *Game) HandleReadCommands(in chan GameCommandInterface) {
if p, ok := e.(*GameCommandUpdateMatrix); ok {
for player, m := range p.Matrixes {
if player == g.LocalPlayer {
g.Players[player].Matrix.GarbageSent = m.GarbageSent
g.Players[player].Matrix.GarbageReceived = m.GarbageReceived
continue
} else if _, ok := g.Players[player]; !ok {
continue
@ -557,6 +581,8 @@ func (g *Game) handle() {
g.Players[g.LocalPlayer].Matrix.SetGameOver()
g.out(&GameCommandGameOver{})
} else if ev, ok := e.(*event.NicknameEvent); ok {
g.out(&GameCommandNickname{Nickname: ev.Nickname})
} else if ev, ok := e.(*event.SendGarbageEvent); ok {
g.out(&GameCommandSendGarbage{Lines: ev.Lines})
} else if ev, ok := e.(*event.ScoreEvent); ok {

View File

@ -223,6 +223,17 @@ 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

View File

@ -131,18 +131,6 @@ func (s *Server) FindGame(p *Player, gameID int) *Game {
g.Lock()
g.AddPlayerL(p)
if len(g.Players) > 1 {
var players []string
for playerID, player := range g.Players {
if playerID == p.Player {
continue
}
players = append(players, player.Name)
}
p.Write(&GameCommandMessage{Message: "Joined game - Players: " + strings.Join(players, " ")})
}
if gameID == -1 {
go g.Start(0)
@ -201,10 +189,6 @@ func (s *Server) handleJoinGame(pl *Player) {
if p, ok := e.(*GameCommandJoinGame); ok {
pl.Name = Nickname(p.Name)
s.Log("JOINING GAME", p)
pl.Write(&GameCommandMessage{Message: "Welcome to netris"})
g := s.FindGame(pl, p.GameID)
s.Log("New player added to game", *pl, p.GameID)
@ -234,7 +218,6 @@ func (s *Server) initiateAutoStart(g *Game) {
}
func (s *Server) handleGameCommands(pl *Player, g *Game) {
s.Log("waiting first msg handle game commands")
for e := range pl.In {
c := e.Command()
if (c != CommandPing && c != CommandPong && c != CommandUpdateMatrix) || g.LogLevel >= LogVerbose {
@ -255,6 +238,19 @@ func (s *Server) handleGameCommands(pl *Player, g *Game) {
}
}
}
case CommandNickname:
if p, ok := e.(*GameCommandNickname); ok {
if player, ok := g.Players[p.SourcePlayer]; ok {
newNick := Nickname(p.Nickname)
if newNick != "" && newNick != player.Name {
oldNick := player.Name
player.Name = newNick
g.Logf(LogStandard, "* %s is now known as %s", oldNick, newNick)
g.WriteAllL(&GameCommandNickname{Player: p.SourcePlayer, Nickname: newNick})
}
}
}
case CommandUpdateMatrix:
if p, ok := e.(*GameCommandUpdateMatrix); ok {
for _, m := range p.Matrixes {
@ -310,7 +306,7 @@ func (s *Server) Listen(address string) {
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error: ", err)
continue
}
s.NewPlayers <- &IncomingPlayer{Name: "Anonymous", Conn: NewServerConn(conn, nil)}

View File

@ -68,7 +68,7 @@ func Connect(address string) *ServerConn {
conn, err = net.DialTimeout(network, address, ConnTimeout)
if err != nil {
if tries > 25 {
log.Fatal("Listen error: ", err)
log.Fatalf("failed to connect to %s: %s", address, err)
} else {
time.Sleep(250 * time.Millisecond)
@ -171,6 +171,14 @@ func (s *ServerConn) handleRead() {
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
@ -291,6 +299,11 @@ func (s *ServerConn) handleWrite() {
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 {

View File

@ -13,6 +13,14 @@ import (
const GarbageDelay = 1500 * time.Millisecond
type MatrixType int
const (
MatrixStandard MatrixType = iota
MatrixPreview
MatrixCustom
)
type Matrix struct {
W int `json:"-"` // Width
H int `json:"-"` // Height
@ -25,7 +33,7 @@ type Matrix struct {
P *Piece
PlayerName string
Preview bool
Type MatrixType
Event chan<- interface{} `json:"-"`
Move chan int `json:"-"`
@ -56,8 +64,8 @@ func I(x int, y int, w int) int {
return (y * w) + x
}
func NewMatrix(w int, h int, b int, players int, event chan<- interface{}, draw chan event.DrawObject, preview bool) *Matrix {
m := Matrix{W: w, H: h, B: b, M: make(map[int]Block), O: make(map[int]Block), Event: event, draw: draw, Preview: preview}
func NewMatrix(w int, h int, b int, players int, event chan<- interface{}, draw chan event.DrawObject, t MatrixType) *Matrix {
m := Matrix{W: w, H: h, B: b, M: make(map[int]Block), O: make(map[int]Block), Event: event, draw: draw, Type: t}
m.Move = make(chan int, 10)
@ -102,7 +110,7 @@ func (m *Matrix) AttachBag(bag *Bag) bool {
}
func (m *Matrix) takePiece() bool {
if m.Preview {
if m.Type != MatrixStandard {
return true
} else if m.GameOver || m.Bag == nil {
return false
@ -410,6 +418,10 @@ func (m *Matrix) DrawPieces() {
}
func (m *Matrix) DrawPiecesL() {
if m.Type != MatrixStandard {
return
}
m.clearOverlay()
if m.GameOver {
@ -505,6 +517,10 @@ func (m *Matrix) SetGameOver() {
}
func (m *Matrix) SetBlock(x int, y int, block Block, overlay bool) bool {
if x < 0 || x >= m.W || y < 0 || y >= m.H+m.B {
return false
}
index := I(x, y, m.W)
if overlay {
@ -873,7 +889,10 @@ func (m *Matrix) Replace(newmtx *Matrix) {
m.M = newmtx.M
m.P = newmtx.P
m.Preview = newmtx.Preview
m.PlayerName = newmtx.PlayerName
m.GarbageSent = newmtx.GarbageSent
m.GarbageReceived = newmtx.GarbageReceived
m.Speed = newmtx.Speed
}
@ -902,7 +921,7 @@ func NewTestMatrix() (*Matrix, error) {
}
}()
m := NewMatrix(10, 20, 20, 1, ev, draw, false)
m := NewMatrix(10, 20, 20, 1, ev, draw, MatrixStandard)
bag, err := NewBag(1, minos, 10)
if err != nil {