This commit is contained in:
Beric Bearnson 2024-10-03 22:41:33 -06:00
parent 15e7f20f1a
commit 067d22f3a9
20 changed files with 477 additions and 258 deletions

51
.air_client.toml Normal file
View File

@ -0,0 +1,51 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/client/main"
cmd = "go build -o ./tmp/client/ ./cmd/client/main.go"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

51
.air_server.toml Normal file
View File

@ -0,0 +1,51 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/server/main"
cmd = "go build -o ./tmp/server/ ./cmd/server/main.go"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@ -8,6 +8,7 @@ import (
"net" "net"
"os" "os"
"sshpong/internal/client" "sshpong/internal/client"
"sshpong/internal/config"
"sshpong/internal/lobby" "sshpong/internal/lobby"
"strings" "strings"
) )
@ -15,25 +16,43 @@ import (
var username string var username string
func main() { func main() {
slog.SetLogLoggerLevel(slog.LevelDebug) if len(os.Args) == 1 {
slog.Debug("Debug logs active...") config.LoadConfig("")
} else {
config.LoadConfig(os.Args[1])
}
slog.SetLogLoggerLevel(slog.Level(config.Config.LogLevel))
fmt.Println("Welcome to sshpong!") fmt.Println("Welcome to sshpong!")
fmt.Println("Please enter your username")
egress := make(chan []byte) egress := make(chan []byte)
ingress := make(chan []byte) ingress := make(chan []byte)
interrupter := make(chan client.InterrupterMessage, 100) interrupter := make(chan client.InterrupterMessage, 100)
exit := make(chan string) exit := make(chan string)
var usernameOk = false
// In the future make a DB call as well?
isUsernameOk := func(un string) bool {
if strings.Contains(un, ":") || len(strings.Split(un, " ")) > 1 || len(un) < 1 {
fmt.Println(client.Red, "Sorry, please pick a username that has no special characters or spaces.", client.Normal)
return false
}
return true
}
for !usernameOk {
fmt.Println("Please enter your username")
buf := make([]byte, 1024) buf := make([]byte, 1024)
n, err := os.Stdin.Read(buf) n, err := os.Stdin.Read(buf)
if err != nil { if err != nil {
log.Panic("Bro your input is no good...") log.Panic("Bro your input is no good...")
} }
username = string(buf[:n-1]) username = string(buf[:n-1])
usernameOk = isUsernameOk(username)
}
fmt.Println("username is...", username)
conn, err := ConnectToLobby(username) conn, err := ConnectToLobby(username)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
@ -55,6 +74,11 @@ func main() {
select { select {
case msg := <-interrupter: case msg := <-interrupter:
if msg.InterruptType == "start_game" {
slog.Debug("closing input handler with start_game message and sending exit signal")
exit <- msg.Content
return
}
userMessage, err := client.HandleInterruptInput(msg, args, username) userMessage, err := client.HandleInterruptInput(msg, args, username)
if err != nil { if err != nil {
userMessage, err = client.HandleUserInput(args, username) userMessage, err = client.HandleUserInput(args, username)
@ -67,16 +91,6 @@ func main() {
} }
} }
egress <- userMessage egress <- userMessage
if userMessage[0] == lobby.Accept || userMessage[0] == lobby.Disconnect {
slog.Debug("Closing input handler with accept or disconnect message")
return
}
if userMessage[0] == lobby.StartGame {
slog.Debug("closing input handler with start_game message and sending exit signal")
exit <- msg.Content
return
}
default: default:
userMessage, err = client.HandleUserInput(args, username) userMessage, err = client.HandleUserInput(args, username)
@ -88,9 +102,7 @@ func main() {
continue continue
} }
egress <- userMessage egress <- userMessage
} }
} }
}() }()
@ -103,6 +115,9 @@ func main() {
if err != nil { if err != nil {
log.Panic("Error handling server message disconnecting...") log.Panic("Error handling server message disconnecting...")
} }
if interrupterMsg.InterruptType == "start_game" {
exit <- interrupterMsg.Content
}
if interrupterMsg.InterruptType != "" { if interrupterMsg.InterruptType != "" {
interrupter <- interrupterMsg interrupter <- interrupterMsg
} }
@ -120,10 +135,6 @@ func main() {
if err == io.EOF { if err == io.EOF {
log.Panic("Server disconnected, sorry...") log.Panic("Server disconnected, sorry...")
} }
if msg[0] == lobby.StartGame || msg[0] == lobby.Disconnect {
slog.Debug("closing network writer ")
return
}
} }
}() }()
@ -139,7 +150,6 @@ func main() {
} }
}() }()
fmt.Println("Waiting for an exit message")
isStartGame := <-exit isStartGame := <-exit
if isStartGame != "" { if isStartGame != "" {
fmt.Println("Connecting to game", isStartGame) fmt.Println("Connecting to game", isStartGame)
@ -157,6 +167,7 @@ func main() {
} }
func ConnectToLobby(username string) (net.Conn, error) { func ConnectToLobby(username string) (net.Conn, error) {
slog.Debug("connecting to server...")
conn, err := net.Dial("tcp", "127.0.0.1:12345") conn, err := net.Dial("tcp", "127.0.0.1:12345")
if err != nil { if err != nil {
return nil, fmt.Errorf("Sorry, failed to connect to server...") return nil, fmt.Errorf("Sorry, failed to connect to server...")

View File

@ -5,7 +5,11 @@ import (
"log" "log"
"log/slog" "log/slog"
"net" "net"
"os"
"sshpong/internal/config"
"sshpong/internal/lobby" "sshpong/internal/lobby"
"sshpong/internal/pong"
"strings"
"sync" "sync"
) )
@ -13,14 +17,20 @@ var exit chan bool
var games sync.Map var games sync.Map
func main() { func main() {
slog.SetLogLoggerLevel(slog.LevelDebug) if len(os.Args) == 1 {
config.LoadConfig("")
} else {
config.LoadConfig(os.Args[1])
}
slog.SetLogLoggerLevel(slog.Level(config.Config.LogLevel))
fmt.Println("Starting sshpong lobby...") fmt.Println("Starting sshpong lobby...")
go LobbyListen() go LobbyListen()
fmt.Println("Lobby started") fmt.Println("Lobby started")
// fmt.Println("Starting game listener...") fmt.Println("Starting game listener...")
// go GamesListen() go GamesListen()
// fmt.Println("Game listener started") fmt.Println("Game listener started")
_ = <-exit _ = <-exit
} }
@ -28,10 +38,9 @@ func main() {
// Starts listening on port 12345 for TCP connections // Starts listening on port 12345 for TCP connections
// Also creates client pool and game connection singletons // Also creates client pool and game connection singletons
func LobbyListen() { func LobbyListen() {
listener, err := net.Listen("tcp", "127.0.0.1:12345") listener, err := net.Listen("tcp", "127.0.0.1:12345")
if err != nil { if err != nil {
slog.Error("Error setting up listener for lobby. Exiting...") slog.Error("Error setting up listener for lobby. Exiting...", err)
} }
defer listener.Close() defer listener.Close()
@ -64,60 +73,62 @@ func LobbyListen() {
} }
} }
// func GamesListen() { func GamesListen() {
//
// slog.SetLogLoggerLevel(slog.LevelDebug) type GameClients struct {
// slog.Debug("Debug level logs are active") Client1 lobby.Client
// Client2 lobby.Client
// gameListener, err := net.Listen("tcp", "127.0.0.1:42069") }
// if err != nil {
// log.Fatal(err) gameListener, err := net.Listen("tcp", "127.0.0.1:42069")
// } if err != nil {
// log.Fatal(err)
// for { }
// defer gameListener.Close()
// conn, err := gameListener.Accept() for {
// if err != nil { defer gameListener.Close()
// log.Println(err) conn, err := gameListener.Accept()
// continue if err != nil {
// } log.Println(err)
// continue
// slog.Debug("Received game connection") }
//
// go func(conn net.Conn) { slog.Debug("Received game connection")
// messageBytes := make([]byte, 126)
// go func(conn net.Conn) {
// n, err := conn.Read(messageBytes) messageBytes := make([]byte, 126)
// if err != nil {
// log.Printf("Error reading game ID on connection %s", err) n, err := conn.Read(messageBytes)
// } if err != nil {
// log.Printf("Error reading game ID on connection %s", err)
// gInfo := strings.SplitAfter(string(messageBytes[:n]), ":") }
// if err != nil {
// log.Printf("Game id was not a string? %s", err) gInfo := strings.SplitAfter(string(messageBytes[:n]), ":")
// } if err != nil {
// log.Printf("Game id was not a string? %s", err)
// slog.Debug("Game request data", slog.Any("game info", gInfo)) }
//
// game, ok := games.Load(gInfo[0]) slog.Debug("Game request data", slog.Any("game info", gInfo))
// if !ok {
// games.Store(gInfo[0], GameClients{Client1: Client{ game, ok := games.Load(gInfo[0])
// Username: gInfo[1], if !ok {
// Conn: conn, games.Store(gInfo[0], GameClients{Client1: lobby.Client{
// }, Client2: Client{}}) Username: gInfo[1],
// } else { Conn: conn,
// gameclients, _ := game.(GameClients) }, Client2: lobby.Client{}})
// client2 := Client{ } else {
// Username: gInfo[1], gameclients, _ := game.(GameClients)
// Conn: conn, client2 := lobby.Client{
// } Username: gInfo[1],
// Conn: conn,
// games.Store(gInfo[0], GameClients{ }
// Client1: gameclients.Client1,
// Client2: client2}) games.Store(gInfo[0], GameClients{
// Client1: gameclients.Client1,
// go pong.StartGame(gameclients.Client1.Conn, client2.Conn, gameclients.Client1.Username, client2.Username) Client2: client2})
// }
// }(conn) go pong.StartGame(gameclients.Client1.Conn, client2.Conn, gameclients.Client1.Username, client2.Username)
// } }
// } }(conn)
}
}

3
config.json Normal file
View File

@ -0,0 +1,3 @@
{
"logLevel": 1
}

7
go.mod
View File

@ -1,11 +1,12 @@
module sshpong module sshpong
go 1.22.2 go 1.23.1
require ( require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
golang.org/x/sys v0.23.0 // indirect golang.org/x/term v0.22.0
golang.org/x/term v0.22.0 // indirect
google.golang.org/protobuf v1.34.2 google.golang.org/protobuf v1.34.2
) )
require golang.org/x/sys v0.23.0 // indirect

2
go.sum
View File

@ -1,3 +1,5 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=

View File

@ -2,6 +2,8 @@ package ansii
import ( import (
"fmt" "fmt"
"log/slog"
"math"
"os" "os"
"strings" "strings"
@ -37,8 +39,8 @@ const (
) )
type Offset struct { type Offset struct {
X int X float32
Y int Y float32
} }
type style struct { type style struct {
@ -89,8 +91,8 @@ func RestoreTerm(prev *term.State) error {
return term.Restore(fd, prev) return term.Restore(fd, prev)
} }
func (s screen) PlaceCursor(offset Offset) ANSI { func (s screen) PlaceCursor(X, Y int) ANSI {
return ANSI(fmt.Sprintf("\033[%d;%dH", offset.Y, offset.X)) return ANSI(fmt.Sprintf("\033[%d;%dH", Y, X))
} }
var ( var (
@ -105,31 +107,34 @@ var (
// Blocks that would be placed off screen are clipped. // Blocks that would be placed off screen are clipped.
func DrawBox(builder *strings.Builder, offset Offset, height int, width int, style ANSI) { func DrawBox(builder *strings.Builder, offset Offset, height int, width int, style ANSI) {
builder.WriteString(string(style)) builder.WriteString(string(style))
for hIdx := 0; hIdx < height; hIdx++ { for hIdx := range height {
if hIdx == 0 || hIdx == height-1 { if hIdx == 0 || hIdx == height-1 {
for wIdx := 0; wIdx < width; wIdx++ { for wIdx := range width {
DrawPixel(builder, Offset{X: offset.X + wIdx, Y: offset.Y + hIdx}) drawPixel(builder, offset.X+float32(wIdx), offset.Y+float32(hIdx))
} }
} else { } else {
DrawPixel(builder, Offset{X: offset.X, Y: offset.Y + hIdx}) drawPixel(builder, offset.X, offset.Y+float32(hIdx))
DrawPixel(builder, Offset{X: offset.X + width - 1, Y: offset.Y + hIdx}) drawPixel(builder, offset.X+float32(width-1), offset.Y+float32(hIdx))
} }
} }
builder.WriteString(string(Styles.Reset)) builder.WriteString(string(Styles.Reset))
return return
} }
func DrawPixel(builder *strings.Builder, offset Offset) { func drawPixel(builder *strings.Builder, offsetX, offsetY float32) {
termWidth, termHeight := GetTermSize() termWidth, termHeight := GetTermSize()
if offset.X > termWidth || offset.Y > termHeight || offset.X < 0 || offset.Y < 0 {
return scaledX := math.Floor(float64(offsetX * float32(termWidth)))
} scaledY := math.Floor(float64(offsetY * float32(termHeight)))
builder.WriteString(string(Screen.PlaceCursor(offset) + ANSI(Blocks.Block)))
slog.Debug("positions", slog.Any("x", scaledX), slog.Any("y", scaledY))
// TODO: Does float to int convertion cause many problems?
builder.WriteString(string(Screen.PlaceCursor(int(scaledX), int(scaledY)) + ANSI(Blocks.Block)))
} }
func DrawPixelStyle(builder *strings.Builder, offset Offset, style ANSI) { func DrawPixelStyle(builder *strings.Builder, offset Offset, style ANSI) {
builder.WriteString(string(style)) builder.WriteString(string(style))
DrawPixel(builder, offset) drawPixel(builder, offset.X, offset.Y)
builder.WriteString(string(Styles.Reset)) builder.WriteString(string(Styles.Reset))
} }

View File

@ -7,6 +7,8 @@ import (
"log/slog" "log/slog"
"sshpong/internal/lobby" "sshpong/internal/lobby"
"strings" "strings"
"github.com/google/uuid"
) )
type InterrupterMessage struct { type InterrupterMessage struct {
@ -16,8 +18,8 @@ type InterrupterMessage struct {
var help = fmt.Errorf("use invite <player name> to invite a player\nchat or / to send a message to the lobby\nq or quit to leave the game") var help = fmt.Errorf("use invite <player name> to invite a player\nchat or / to send a message to the lobby\nq or quit to leave the game")
var red = "\x1b[31m" var Red = "\x1b[31m"
var normal = "\033[0m" var Normal = "\033[0m"
func HandleUserInput(args []string, username string) ([]byte, error) { func HandleUserInput(args []string, username string) ([]byte, error) {
if len(args) == 0 { if len(args) == 0 {
@ -26,11 +28,15 @@ func HandleUserInput(args []string, username string) ([]byte, error) {
switch args[0] { switch args[0] {
case "invite": case "invite":
if args[1] != "" { if args[1] != "" {
if args[1] == username {
fmt.Println("You cannot invite yourself to a game ;)")
} else {
msg, err := lobby.Marshal(lobby.InviteData{From: username, To: args[1]}, lobby.Invite) msg, err := lobby.Marshal(lobby.InviteData{From: username, To: args[1]}, lobby.Invite)
if err != nil { if err != nil {
slog.Debug("invite message was not properly marshalled", "error", err) slog.Debug("invite message was not properly marshalled", "error", err)
} }
return msg, err return msg, err
}
} else { } else {
fmt.Println("Please provide a player to invite ") fmt.Println("Please provide a player to invite ")
} }
@ -84,6 +90,7 @@ func HandleInterruptInput(incoming InterrupterMessage, args []string, username s
switch incoming.InterruptType { switch incoming.InterruptType {
// Respond with yes if you accept game // Respond with yes if you accept game
case "invite": case "invite":
slog.Debug("handling invite interrupt")
if len(args) < 1 { if len(args) < 1 {
return []byte{}, nil return []byte{}, nil
} else { } else {
@ -91,6 +98,7 @@ func HandleInterruptInput(incoming InterrupterMessage, args []string, username s
msg, err := lobby.Marshal(lobby.AcceptData{ msg, err := lobby.Marshal(lobby.AcceptData{
From: username, From: username,
To: incoming.Content, To: incoming.Content,
GameID: uuid.NewString(),
}, lobby.Accept) }, lobby.Accept)
if err != nil { if err != nil {
slog.Debug("accept message was not properly marshalled", "error", err) slog.Debug("accept message was not properly marshalled", "error", err)
@ -99,20 +107,21 @@ func HandleInterruptInput(incoming InterrupterMessage, args []string, username s
} }
} }
// Disconnect and connect to game // TODO: Do we need this accepted? Disconnect and connect to game
case "accepted": // case "accepted":
msg, err := lobby.Marshal(lobby.DisconnectData{ // msg, err := lobby.Marshal(lobby.DisconnectData{
From: incoming.Content, // From: incoming.Content,
}, lobby.Disconnect) // }, lobby.Disconnect)
if err != nil { // if err != nil {
slog.Debug("disconnect message was not properly marshalled", "error", err) // slog.Debug("disconnect message was not properly marshalled", "error", err)
} // }
return msg, err // return msg, err
case "start_game": case "start_game":
msg, err := lobby.Marshal(lobby.StartGameData{ msg, err := lobby.Marshal(lobby.StartGameData{
To: "", To: "",
GameID: incoming.Content, GameID: incoming.Content,
}, lobby.Chat) }, lobby.StartGame)
if err != nil { if err != nil {
slog.Debug("start game message was not properly marshalled", "error", err) slog.Debug("start game message was not properly marshalled", "error", err)
} }
@ -124,7 +133,6 @@ func HandleInterruptInput(incoming InterrupterMessage, args []string, username s
func HandleServerMessage(msg []byte) (InterrupterMessage, error) { func HandleServerMessage(msg []byte) (InterrupterMessage, error) {
header := msg[0] header := msg[0]
switch header { switch header {
case lobby.Invite: case lobby.Invite:
imsg, err := lobby.Unmarshal[lobby.InviteData](msg) imsg, err := lobby.Unmarshal[lobby.InviteData](msg)
@ -160,6 +168,7 @@ func HandleServerMessage(msg []byte) (InterrupterMessage, error) {
if err != nil { if err != nil {
return InterrupterMessage{}, errors.New("Not a properly formatted start game message") return InterrupterMessage{}, errors.New("Not a properly formatted start game message")
} }
fmt.Println("Your invite was accepted. Press Enter to join game")
return InterrupterMessage{ return InterrupterMessage{
InterruptType: "start_game", InterruptType: "start_game",
Content: sgmsg.GameID, Content: sgmsg.GameID,
@ -206,7 +215,7 @@ func HandleServerMessage(msg []byte) (InterrupterMessage, error) {
if err != nil { if err != nil {
slog.Debug("Received an indecipherable error message...", slog.Any("msg", msg[1:])) slog.Debug("Received an indecipherable error message...", slog.Any("msg", msg[1:]))
} }
fmt.Println(red, em.Message, normal) fmt.Println(Red, em.Message, Normal)
} }
return InterrupterMessage{}, nil return InterrupterMessage{}, nil

View File

@ -166,9 +166,9 @@ func handleGameInput(bytes []byte) {
// Up // Up
case 'w': case 'w':
if isPlayer1 { if isPlayer1 {
state.Player1.Pos.Y = state.Player1.Pos.Y + 1 state.Player1.Pos.Y = state.Player1.Pos.Y - 1
v, err := json.Marshal(pong.Vector{ v, err := json.Marshal(pong.Vector{
X: 0, Y: state.Player1.Pos.Y, X: -50, Y: state.Player1.Pos.Y,
}) })
if err != nil { if err != nil {
slog.Debug("error marshalling player movement", slog.Any("error", err)) slog.Debug("error marshalling player movement", slog.Any("error", err))
@ -180,9 +180,9 @@ func handleGameInput(bytes []byte) {
egress <- update egress <- update
} else { } else {
state.Player2.Pos.Y = state.Player2.Pos.Y + 1 state.Player2.Pos.Y = state.Player2.Pos.Y - 1
v, err := json.Marshal(pong.Vector{ v, err := json.Marshal(pong.Vector{
X: 0, Y: state.Player2.Pos.Y, X: 50, Y: state.Player2.Pos.Y,
}) })
if err != nil { if err != nil {
slog.Debug("error marshalling Player2 movement", slog.Any("error", err)) slog.Debug("error marshalling Player2 movement", slog.Any("error", err))
@ -197,9 +197,9 @@ func handleGameInput(bytes []byte) {
// Down // Down
case 's': case 's':
if isPlayer1 { if isPlayer1 {
state.Player1.Pos.Y = state.Player1.Pos.Y - 1 state.Player1.Pos.Y = state.Player1.Pos.Y + 1
v, err := json.Marshal(pong.Vector{ v, err := json.Marshal(pong.Vector{
X: 0, Y: state.Player1.Pos.Y, X: -50, Y: state.Player1.Pos.Y,
}) })
if err != nil { if err != nil {
slog.Debug("error marshalling player movement", slog.Any("error", err)) slog.Debug("error marshalling player movement", slog.Any("error", err))
@ -211,9 +211,9 @@ func handleGameInput(bytes []byte) {
egress <- update egress <- update
} else { } else {
state.Player2.Pos.Y = state.Player2.Pos.Y - 1 state.Player2.Pos.Y = state.Player2.Pos.Y + 1
v, err := json.Marshal(pong.Vector{ v, err := json.Marshal(pong.Vector{
X: 0, Y: state.Player2.Pos.Y, X: 50, Y: state.Player2.Pos.Y,
}) })
if err != nil { if err != nil {
slog.Debug("error marshalling player movement", slog.Any("error", err)) slog.Debug("error marshalling player movement", slog.Any("error", err))
@ -252,9 +252,9 @@ func handleGameInput(bytes []byte) {
// Up // Up
case 65: case 65:
if isPlayer1 { if isPlayer1 {
state.Player1.Pos.Y = state.Player1.Pos.Y + 1 state.Player1.Pos.Y = state.Player1.Pos.Y - 1
v, err := json.Marshal(pong.Vector{ v, err := json.Marshal(pong.Vector{
X: 0, Y: state.Player1.Pos.Y, X: -50, Y: state.Player1.Pos.Y,
}) })
if err != nil { if err != nil {
slog.Debug("error marshalling player movement", slog.Any("error", err)) slog.Debug("error marshalling player movement", slog.Any("error", err))
@ -266,9 +266,9 @@ func handleGameInput(bytes []byte) {
egress <- update egress <- update
} else { } else {
state.Player2.Pos.Y = state.Player2.Pos.Y + 1 state.Player2.Pos.Y = state.Player2.Pos.Y - 1
v, err := json.Marshal(pong.Vector{ v, err := json.Marshal(pong.Vector{
X: 0, Y: state.Player2.Pos.Y, X: 50, Y: state.Player2.Pos.Y,
}) })
if err != nil { if err != nil {
slog.Debug("error marshalling Player2 movement", slog.Any("error", err)) slog.Debug("error marshalling Player2 movement", slog.Any("error", err))
@ -283,9 +283,9 @@ func handleGameInput(bytes []byte) {
// Down // Down
case 66: case 66:
if isPlayer1 { if isPlayer1 {
state.Player1.Pos.Y = state.Player1.Pos.Y - 1 state.Player1.Pos.Y = state.Player1.Pos.Y + 1
v, err := json.Marshal(pong.Vector{ v, err := json.Marshal(pong.Vector{
X: 0, Y: state.Player1.Pos.Y, X: -50, Y: state.Player1.Pos.Y,
}) })
if err != nil { if err != nil {
slog.Debug("error marshalling player movement", slog.Any("error", err)) slog.Debug("error marshalling player movement", slog.Any("error", err))
@ -297,9 +297,9 @@ func handleGameInput(bytes []byte) {
egress <- update egress <- update
} else { } else {
state.Player2.Pos.Y = state.Player2.Pos.Y - 1 state.Player2.Pos.Y = state.Player2.Pos.Y + 1
v, err := json.Marshal(pong.Vector{ v, err := json.Marshal(pong.Vector{
X: 0, Y: state.Player2.Pos.Y, X: 50, Y: state.Player2.Pos.Y,
}) })
if err != nil { if err != nil {
slog.Debug("error marshalling player movement", slog.Any("error", err)) slog.Debug("error marshalling player movement", slog.Any("error", err))

36
internal/config/config.go Normal file
View File

@ -0,0 +1,36 @@
package config
import (
"encoding/json"
"log/slog"
"os"
)
var Config Configuration
type Configuration struct {
LogLevel int `json:"logLevel"`
}
func LoadConfig(path string) {
var c = Configuration{}
var cf []byte
var err error
if path != "" {
cf, err = os.ReadFile(path)
} else {
cf, err = os.ReadFile("config.json")
}
if err != nil {
slog.Info("failed to open config at path provided, using default config instead")
}
err = json.Unmarshal(cf, &c)
if err != nil {
slog.Info("failed to read configuration, using default config instead...", err)
}
Config = c
return
}

View File

@ -6,8 +6,6 @@ import (
"log/slog" "log/slog"
"net" "net"
"sync" "sync"
"github.com/google/uuid"
) )
type Lobby struct { type Lobby struct {
@ -37,6 +35,7 @@ func CreateLobby() *Lobby {
go func(lm *sync.Map) { go func(lm *sync.Map) {
for { for {
msg := <-externalMessageChan msg := <-externalMessageChan
slog.Debug("forwarding external message")
tc, ok := lm.Load(msg.Target) tc, ok := lm.Load(msg.Target)
if !ok { if !ok {
@ -170,11 +169,18 @@ func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) {
return []byte{}, err return []byte{}, err
} }
_, ok := l.lobbyMembers.Load(i.To)
if !ok {
return Marshal(ErrorData{
Message: fmt.Sprintf("Sorry, player %s is not available.", i.To),
}, Error)
} else {
l.ExternalMessageChannel <- ExternalMessage{ l.ExternalMessageChannel <- ExternalMessage{
From: i.From, From: i.From,
Target: i.To, Target: i.To,
Message: msg, Message: msg,
} }
}
return Marshal(PendingInviteData{ return Marshal(PendingInviteData{
Recipient: i.To, Recipient: i.To,
@ -195,12 +201,11 @@ func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) {
return []byte{}, err return []byte{}, err
} }
gID := uuid.NewString() gID := a.GameID
msg, err := Marshal(AcceptedData{ msg, err := Marshal(StartGameData{
Accepter: a.From,
GameID: gID, GameID: gID,
}, Accepted) }, StartGame)
l.ExternalMessageChannel <- ExternalMessage{ l.ExternalMessageChannel <- ExternalMessage{
From: a.From, From: a.From,
@ -208,23 +213,25 @@ func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) {
Message: msg, Message: msg,
} }
slog.Debug("Sent start game message to inviter")
return Marshal(StartGameData{ return Marshal(StartGameData{
To: a.From, To: a.From,
From: a.To,
GameID: gID, GameID: gID,
}, StartGame) }, StartGame)
case Accepted:
a, err := Unmarshal[AcceptedData](msg)
if err != nil {
slog.Debug("error unmarshalling accpeted message", "error", err)
return []byte{}, err
}
// TODO: figure out the accepted and start game data situation... To field is a little hard to fill. // TODO: figure out the accepted and start game data situation... To field is a little hard to fill.
return Marshal(StartGameData{ // case Accepted:
To: "", // a, err := Unmarshal[AcceptedData](msg)
GameID: a.GameID, // if err != nil {
}, StartGame) // slog.Debug("error unmarshalling accpeted message", "error", err)
// return []byte{}, err
// }
// return Marshal(StartGameData{
// To: "",
// GameID: a.GameID,
// }, StartGame)
// TODO: Like pending invite, I think start game is only a client message // TODO: Like pending invite, I think start game is only a client message
// case StartGame: // case StartGame:

View File

@ -38,16 +38,18 @@ type PendingInviteData struct {
type AcceptData struct { type AcceptData struct {
From string `json:"from"` From string `json:"from"`
To string `json:"to"` To string `json:"to"`
GameID string `json:"gameID"`
} }
type AcceptedData struct { type AcceptedData struct {
Accepter string `json:"accepter"` Accepter string `json:"accepter"`
GameID string `json:"game_id"` GameID string `json:"gameID"`
} }
type StartGameData struct { type StartGameData struct {
To string `json:"to"` To string `json:"to"`
GameID string `json:"game_id"` From string `json:"from"`
GameID string `json:"gameID"`
} }
type DeclineData struct { type DeclineData struct {

View File

@ -23,7 +23,7 @@ var player2 GameClient
var ingress chan StateUpdate var ingress chan StateUpdate
var egress chan StateUpdate var egress chan StateUpdate
const posXBound = 100 const posXBound = 52
const negXBound = posXBound * -1 const negXBound = posXBound * -1
const posYBound = 50 const posYBound = 50
const negYBound = posYBound * -1 const negYBound = posYBound * -1
@ -141,6 +141,9 @@ func gameLoop(state *GameState) {
return return
} }
ingress <- msg ingress <- msg
if msg.FieldPath == "Winner" {
return
}
} }
}() }()
@ -157,6 +160,9 @@ func gameLoop(state *GameState) {
return return
} }
ingress <- msg ingress <- msg
if msg.FieldPath == "Winner" {
return
}
} }
}() }()
@ -164,6 +170,9 @@ func gameLoop(state *GameState) {
for { for {
msg := <-egress msg := <-egress
broadcastUpdate(msg) broadcastUpdate(msg)
if msg.FieldPath == "Winner" {
return
}
} }
}() }()
@ -176,17 +185,23 @@ func gameLoop(state *GameState) {
if err != nil { if err != nil {
fmt.Println("FUCK!~", err) fmt.Println("FUCK!~", err)
} }
if msg.FieldPath == "Winner" {
slog.Debug("Closing game loop on winner message")
return
}
case _ = <-ticker.C: case _ = <-ticker.C:
update := process(state) update := process(state)
egress <- update egress <- update
if update.FieldPath == "Winner" {
slog.Debug("Closing game loop")
return
}
} }
} }
} }
func process(state *GameState) StateUpdate { func process(state *GameState) StateUpdate {
// Move players // Move players
// Check if player edge is out of bounds // Check if player edge is out of bounds
// If out of bounds reset velocity to zero and position to edge // If out of bounds reset velocity to zero and position to edge
@ -229,11 +244,13 @@ func process(state *GameState) StateUpdate {
if state.Ball.Pos.X <= negXBound+1 && state.Ball.Vel.X < 0 { if state.Ball.Pos.X <= negXBound+1 && state.Ball.Vel.X < 0 {
// Paddle hit! // Paddle hit!
if state.Ball.Pos.Y <= state.Player1.Pos.Y+state.Player1.Size.Y && state.Ball.Pos.Y >= state.Player1.Pos.Y-state.Player1.Size.Y { if state.Ball.Pos.Y <= state.Player1.Pos.Y+state.Player1.Size.Y && state.Ball.Pos.Y >= state.Player1.Pos.Y-state.Player1.Size.Y {
slog.Debug("Player1 paddle hit!")
state.Ball.Pos.X = (negXBound + 1) - (state.Ball.Pos.X - (negXBound + 1)) state.Ball.Pos.X = (negXBound + 1) - (state.Ball.Pos.X - (negXBound + 1))
state.Ball.Vel.X = state.Ball.Vel.X * -1.001 state.Ball.Vel.X = state.Ball.Vel.X * -1.001
angleTweak := (state.Ball.Pos.Y - state.Player2.Pos.Y) / (state.Player2.Size.Y / 2) angleTweak := (state.Ball.Pos.Y - state.Player1.Pos.Y) / (state.Player1.Size.Y / 2)
state.Ball.Vel.Y = state.Ball.Vel.Y * angleTweak state.Ball.Vel.Y = angleTweak / 10
} else { } else {
slog.Debug("Player1 paddle miss...")
state.Ball.Pos.X = 0 state.Ball.Pos.X = 0
state.Ball.Pos.Y = 0 state.Ball.Pos.Y = 0
state.Ball.Vel.X = 1 state.Ball.Vel.X = 1
@ -252,11 +269,13 @@ func process(state *GameState) StateUpdate {
if state.Ball.Pos.X > posXBound-1 && state.Ball.Vel.X > 0 { if state.Ball.Pos.X > posXBound-1 && state.Ball.Vel.X > 0 {
// Paddle hit! // Paddle hit!
if state.Ball.Pos.Y <= state.Player2.Pos.Y+state.Player2.Size.Y && state.Ball.Pos.Y >= state.Player2.Pos.Y-state.Player2.Size.Y { if state.Ball.Pos.Y <= state.Player2.Pos.Y+state.Player2.Size.Y && state.Ball.Pos.Y >= state.Player2.Pos.Y-state.Player2.Size.Y {
slog.Debug("Player2 paddle hit!")
state.Ball.Pos.X = (posXBound - 1) - (state.Ball.Pos.X - (posXBound - 1)) state.Ball.Pos.X = (posXBound - 1) - (state.Ball.Pos.X - (posXBound - 1))
state.Ball.Vel.X = state.Ball.Vel.X * -1.001 state.Ball.Vel.X = state.Ball.Vel.X * -1.001
angleTweak := (state.Ball.Pos.Y - state.Player2.Pos.Y) / (state.Player2.Size.Y / 2) angleTweak := (state.Ball.Pos.Y - state.Player2.Pos.Y) / (state.Player2.Size.Y / 2)
state.Ball.Vel.Y = state.Ball.Vel.Y * angleTweak state.Ball.Vel.Y = angleTweak / 10
} else { } else {
slog.Debug("Player2 paddle miss...")
state.Ball.Pos.X = 0 state.Ball.Pos.X = 0
state.Ball.Pos.Y = 0 state.Ball.Pos.Y = 0
state.Ball.Vel.X = -1 state.Ball.Vel.X = -1

View File

@ -6,7 +6,8 @@ import (
"sshpong/internal/ansii" "sshpong/internal/ansii"
"sshpong/internal/pong" "sshpong/internal/pong"
"strings" "strings"
"time"
"golang.org/x/term"
) )
var ( var (
@ -15,96 +16,88 @@ var (
millisecondTimeFrame float64 = float64(1 / targetFpMilli) millisecondTimeFrame float64 = float64(1 / targetFpMilli)
quit chan bool quit chan bool
userInput chan rune userInput chan rune
playerX int = 10 )
playerY int = 10
const (
reset string = "\033[0m"
plain string = ""
bold string = "\033[1m"
underline string = "\033[4m"
black string = "\033[30m"
red string = "\033[31m"
green string = "\033[32m"
yellow string = "\033[33m"
blue string = "\033[34m"
purple string = "\033[35m"
cyan string = "\033[36m"
white string = "\033[37m"
blackBg string = "\033[40m"
redBg string = "\033[41m"
greenBg string = "\033[42m"
yellowBg string = "\033[43m"
blueBg string = "\033[44m"
purpleBg string = "\033[45m"
cyanBg string = "\033[46m"
whiteBg string = "\033[47m"
clearScreen string = "\033[2J"
hideCursor string = "\033[?25l"
showCursor string = "\033[?25h"
) )
func Render(state pong.GameState) { func Render(state pong.GameState) {
// drawScreen(state) // fmt.Println("Player 1", ((state.Player1.Pos.X+50)/100)*width, ((state.Player1.Pos.Y+50)/100)*height)
fmt.Print("\033c") // fmt.Println("Player 2", ((state.Player2.Pos.X+50)/100)*width, ((state.Player2.Pos.Y+50)/100)*height)
fmt.Println("Player 1", state.Player1.Pos.X, state.Player1.Pos.Y) // fmt.Println("Ball", ((state.Ball.Pos.X+50)/100)*width, ((state.Ball.Pos.Y+50)/100)*height)
fmt.Println("Player 2", state.Player2.Pos.X, state.Player2.Pos.Y)
}
func writeCheckerBoard(height int, width int, builder *strings.Builder) {
for i := 0; i < height; i++ {
for j := 0; j < width; j++ {
if i%2 == 0 {
if j%2 == 0 {
builder.WriteString("█")
} else {
builder.WriteString(" ")
}
} else {
if j%2 == 0 {
builder.WriteString(" ")
} else {
builder.WriteString("█")
}
}
}
}
}
func drawScreen(state pong.GameState) {
// width := 100
// height := 50
var builder = strings.Builder{} var builder = strings.Builder{}
builder.WriteString(string(ansii.Screen.ClearScreen)) builder.WriteString(string(ansii.Screen.ClearScreen))
ansii.DrawBox(&builder, ansii.Offset{X: int(state.Player1.Pos.X), Y: int(state.Player1.Pos.Y)}, 5, 1, ansii.Colors.Cyan) x1, y1 := transformToTermPos(state.Player1.Pos)
ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player1.Pos.X), Y: int(state.Player1.Pos.Y)}, ansii.Colors.Purple) builder.WriteString(renderBox(&builder, x1+2, y1, 2, 10, cyan))
ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player1.Pos.X), Y: int(state.Player1.Pos.Y) + 5}, ansii.Colors.Purple)
ansii.DrawBox(&builder, ansii.Offset{X: int(state.Player2.Pos.X), Y: int(state.Player2.Pos.Y)}, 5, 1, ansii.Colors.Cyan) x2, y2 := transformToTermPos(state.Player2.Pos)
ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player2.Pos.X), Y: int(state.Player2.Pos.Y)}, ansii.Colors.Purple) builder.WriteString(renderBox(&builder, x2, y2, 2, 10, purple))
ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player2.Pos.X), Y: int(state.Player2.Pos.Y) + 5}, ansii.Colors.Purple)
// Quit instructions xb, yb := transformToTermPos(state.Ball.Pos)
// builder.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: 0, Y: height}))) builder.WriteString(renderPixel(&builder, xb, yb, red))
// builder.WriteString("q to quit")
builder.WriteString(renderMessage(&builder, state.Message))
os.Stdout.WriteString(builder.String()) os.Stdout.WriteString(builder.String())
} }
func drawFrameStats(frameNum int, frameTimeMs float64) { func setCursorPos(x, y int) string {
width, height := ansii.GetTermSize() return fmt.Sprintf("\033[%d;%dH", y, x)
var spareTimeMilli = millisecondTimeFrame - frameTimeMs
os.Stdout.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: width - 12, Y: height - 2})))
os.Stdout.WriteString(fmt.Sprintf("Frame #: %d", frameNum))
os.Stdout.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: width - 19, Y: height - 1})))
os.Stdout.WriteString(fmt.Sprintf("Frame Time: %.4fms", frameTimeMs))
os.Stdout.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: width - 20, Y: height})))
os.Stdout.WriteString(fmt.Sprintf("Spare Time: %.4fms", spareTimeMilli))
} }
func handleInput(rawInput rune) { // Renders a box with center positioned at X,Y with specified width and height
action := ProcessInput(rawInput) func renderBox(builder *strings.Builder, X, Y, width, height int, style string) string {
width, height := ansii.GetTermSize() str := ""
for x := X - (width / 2); x < X+(width/2); x++ {
switch action { for y := Y - (height / 2); y < Y+(height/2); y++ {
case Quit: str = str + (setCursorPos(x, y) + style + "█")
fmt.Println("Quitting...")
close(quit)
case Left, LeftArrow:
playerX = max(playerX-1, 0)
case Right, RightArrow:
playerX = min(playerX+1, width)
case Up, UpArrow:
playerY = max(playerY-1, 0)
case Down, DownArrow:
playerY = min(playerY+1, height)
case Unknown:
default:
os.Stdout.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: 0, Y: height - 2})))
os.Stdout.WriteString("Unrecognized Input: " + string(action))
close(quit)
} }
}
return str
} }
func waitForFpsLock(startMs float64) { func renderPixel(builder *strings.Builder, x, y int, style string) string {
for { return (setCursorPos(x, y) + style + "█")
var nowMs = float64(time.Now().UnixNano()) / 1_000_000.0 }
if nowMs-startMs >= millisecondTimeFrame {
break func renderMessage(builder *strings.Builder, message string) string {
} xm, xy := transformToTermPos(pong.Vector{X: 40, Y: 40})
} return (setCursorPos(xm, xy) + reset + message)
}
// Returns state x and y positions with center origin and 50 by 50 area
// to scaled, top-left origin coordinates for the user's terminal size.
func transformToTermPos(vec pong.Vector) (int, int) {
iwidth, iheight, _ := term.GetSize(int(os.Stdin.Fd()))
width := float32(iwidth)
height := float32(iheight)
ix := int(((vec.X + 50) / 100) * width)
iy := int(((vec.Y + 50) / 100) * height)
return ix, iy
} }

6
invite_proc.txt Normal file
View File

@ -0,0 +1,6 @@
1. Inviter invites other player
2. Recipent acceptes invite
3. Recipent Sends Accept message with generated GameID, receives StartGame message
3. Recipent handles StartGame message and connects to game instance
4. Inviter recieves StartGame message with GameID
5. Inviter connects to game instance

View File

@ -1,2 +1,13 @@
lint: lint:
golangci-lint run golangci-lint run
server:
air -c .air_server.toml
client:
while true; do \
go run cmd/client/main.go;\
echo "Client has exited, restarting...";\
sleep 2;\
done

1
tmp/build-errors.log Normal file
View File

@ -0,0 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

BIN
tmp/client/main Executable file

Binary file not shown.

BIN
tmp/server/main Executable file

Binary file not shown.