diff --git a/.air_client.toml b/.air_client.toml new file mode 100644 index 0000000..1713665 --- /dev/null +++ b/.air_client.toml @@ -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 diff --git a/.air_server.toml b/.air_server.toml new file mode 100644 index 0000000..a51d099 --- /dev/null +++ b/.air_server.toml @@ -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 diff --git a/cmd/client/main.go b/cmd/client/main.go index 6348e36..f20e9e3 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -8,6 +8,7 @@ import ( "net" "os" "sshpong/internal/client" + "sshpong/internal/config" "sshpong/internal/lobby" "strings" ) @@ -15,25 +16,43 @@ import ( var username string func main() { - slog.SetLogLoggerLevel(slog.LevelDebug) - slog.Debug("Debug logs active...") + if len(os.Args) == 1 { + config.LoadConfig("") + } else { + config.LoadConfig(os.Args[1]) + } + + slog.SetLogLoggerLevel(slog.Level(config.Config.LogLevel)) fmt.Println("Welcome to sshpong!") - fmt.Println("Please enter your username") egress := make(chan []byte) ingress := make(chan []byte) interrupter := make(chan client.InterrupterMessage, 100) exit := make(chan string) - buf := make([]byte, 1024) - n, err := os.Stdin.Read(buf) - if err != nil { - log.Panic("Bro your input is no good...") - } - username = string(buf[:n-1]) + 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) + n, err := os.Stdin.Read(buf) + if err != nil { + log.Panic("Bro your input is no good...") + } + username = string(buf[:n-1]) + usernameOk = isUsernameOk(username) + } - fmt.Println("username is...", username) conn, err := ConnectToLobby(username) if err != nil { log.Panic(err) @@ -55,6 +74,11 @@ func main() { select { 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) if err != nil { userMessage, err = client.HandleUserInput(args, username) @@ -67,16 +91,6 @@ func main() { } } 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: userMessage, err = client.HandleUserInput(args, username) @@ -88,9 +102,7 @@ func main() { continue } egress <- userMessage - } - } }() @@ -103,6 +115,9 @@ func main() { if err != nil { log.Panic("Error handling server message disconnecting...") } + if interrupterMsg.InterruptType == "start_game" { + exit <- interrupterMsg.Content + } if interrupterMsg.InterruptType != "" { interrupter <- interrupterMsg } @@ -120,10 +135,6 @@ func main() { if err == io.EOF { 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 if isStartGame != "" { fmt.Println("Connecting to game", isStartGame) @@ -157,6 +167,7 @@ func main() { } func ConnectToLobby(username string) (net.Conn, error) { + slog.Debug("connecting to server...") conn, err := net.Dial("tcp", "127.0.0.1:12345") if err != nil { return nil, fmt.Errorf("Sorry, failed to connect to server...") diff --git a/cmd/server/main.go b/cmd/server/main.go index d46bfa8..44d2a30 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -5,7 +5,11 @@ import ( "log" "log/slog" "net" + "os" + "sshpong/internal/config" "sshpong/internal/lobby" + "sshpong/internal/pong" + "strings" "sync" ) @@ -13,14 +17,20 @@ var exit chan bool var games sync.Map 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...") go LobbyListen() fmt.Println("Lobby started") - // fmt.Println("Starting game listener...") - // go GamesListen() - // fmt.Println("Game listener started") + fmt.Println("Starting game listener...") + go GamesListen() + fmt.Println("Game listener started") _ = <-exit } @@ -28,10 +38,9 @@ func main() { // Starts listening on port 12345 for TCP connections // Also creates client pool and game connection singletons func LobbyListen() { - listener, err := net.Listen("tcp", "127.0.0.1:12345") 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() @@ -64,60 +73,62 @@ func LobbyListen() { } } -// func GamesListen() { -// -// slog.SetLogLoggerLevel(slog.LevelDebug) -// slog.Debug("Debug level logs are active") -// -// gameListener, err := net.Listen("tcp", "127.0.0.1:42069") -// if err != nil { -// log.Fatal(err) -// } -// -// for { -// defer gameListener.Close() -// conn, err := gameListener.Accept() -// if err != nil { -// log.Println(err) -// continue -// } -// -// slog.Debug("Received game connection") -// -// go func(conn net.Conn) { -// messageBytes := make([]byte, 126) -// -// 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) -// } -// -// slog.Debug("Game request data", slog.Any("game info", gInfo)) -// -// game, ok := games.Load(gInfo[0]) -// if !ok { -// games.Store(gInfo[0], GameClients{Client1: Client{ -// Username: gInfo[1], -// Conn: conn, -// }, Client2: Client{}}) -// } else { -// gameclients, _ := game.(GameClients) -// client2 := Client{ -// Username: gInfo[1], -// Conn: conn, -// } -// -// games.Store(gInfo[0], GameClients{ -// Client1: gameclients.Client1, -// Client2: client2}) -// -// go pong.StartGame(gameclients.Client1.Conn, client2.Conn, gameclients.Client1.Username, client2.Username) -// } -// }(conn) -// } -// } +func GamesListen() { + + type GameClients struct { + Client1 lobby.Client + Client2 lobby.Client + } + + gameListener, err := net.Listen("tcp", "127.0.0.1:42069") + if err != nil { + log.Fatal(err) + } + + for { + defer gameListener.Close() + conn, err := gameListener.Accept() + if err != nil { + log.Println(err) + continue + } + + slog.Debug("Received game connection") + + go func(conn net.Conn) { + messageBytes := make([]byte, 126) + + 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) + } + + slog.Debug("Game request data", slog.Any("game info", gInfo)) + + game, ok := games.Load(gInfo[0]) + if !ok { + games.Store(gInfo[0], GameClients{Client1: lobby.Client{ + Username: gInfo[1], + Conn: conn, + }, Client2: lobby.Client{}}) + } else { + gameclients, _ := game.(GameClients) + client2 := lobby.Client{ + Username: gInfo[1], + Conn: conn, + } + + games.Store(gInfo[0], GameClients{ + Client1: gameclients.Client1, + Client2: client2}) + + go pong.StartGame(gameclients.Client1.Conn, client2.Conn, gameclients.Client1.Username, client2.Username) + } + }(conn) + } +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..adb239e --- /dev/null +++ b/config.json @@ -0,0 +1,3 @@ +{ + "logLevel": 1 +} diff --git a/go.mod b/go.mod index 24f0fbe..2e84c0f 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module sshpong -go 1.22.2 +go 1.23.1 require ( github.com/google/uuid v1.6.0 golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 - golang.org/x/sys v0.23.0 // indirect - golang.org/x/term v0.22.0 // indirect + golang.org/x/term v0.22.0 google.golang.org/protobuf v1.34.2 ) + +require golang.org/x/sys v0.23.0 // indirect diff --git a/go.sum b/go.sum index b977b25..649e91b 100644 --- a/go.sum +++ b/go.sum @@ -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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= diff --git a/internal/ansii/ansii.go b/internal/ansii/ansii.go index 7b37fb7..efa9287 100644 --- a/internal/ansii/ansii.go +++ b/internal/ansii/ansii.go @@ -2,6 +2,8 @@ package ansii import ( "fmt" + "log/slog" + "math" "os" "strings" @@ -37,8 +39,8 @@ const ( ) type Offset struct { - X int - Y int + X float32 + Y float32 } type style struct { @@ -89,8 +91,8 @@ func RestoreTerm(prev *term.State) error { return term.Restore(fd, prev) } -func (s screen) PlaceCursor(offset Offset) ANSI { - return ANSI(fmt.Sprintf("\033[%d;%dH", offset.Y, offset.X)) +func (s screen) PlaceCursor(X, Y int) ANSI { + return ANSI(fmt.Sprintf("\033[%d;%dH", Y, X)) } var ( @@ -105,31 +107,34 @@ var ( // Blocks that would be placed off screen are clipped. func DrawBox(builder *strings.Builder, offset Offset, height int, width int, style ANSI) { builder.WriteString(string(style)) - for hIdx := 0; hIdx < height; hIdx++ { - + for hIdx := range height { if hIdx == 0 || hIdx == height-1 { - for wIdx := 0; wIdx < width; wIdx++ { - DrawPixel(builder, Offset{X: offset.X + wIdx, Y: offset.Y + hIdx}) + for wIdx := range width { + drawPixel(builder, offset.X+float32(wIdx), offset.Y+float32(hIdx)) } } else { - DrawPixel(builder, Offset{X: offset.X, Y: offset.Y + hIdx}) - DrawPixel(builder, Offset{X: offset.X + width - 1, Y: offset.Y + hIdx}) + drawPixel(builder, offset.X, offset.Y+float32(hIdx)) + drawPixel(builder, offset.X+float32(width-1), offset.Y+float32(hIdx)) } } builder.WriteString(string(Styles.Reset)) return } -func DrawPixel(builder *strings.Builder, offset Offset) { +func drawPixel(builder *strings.Builder, offsetX, offsetY float32) { termWidth, termHeight := GetTermSize() - if offset.X > termWidth || offset.Y > termHeight || offset.X < 0 || offset.Y < 0 { - return - } - builder.WriteString(string(Screen.PlaceCursor(offset) + ANSI(Blocks.Block))) + + scaledX := math.Floor(float64(offsetX * float32(termWidth))) + scaledY := math.Floor(float64(offsetY * float32(termHeight))) + + 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) { builder.WriteString(string(style)) - DrawPixel(builder, offset) + drawPixel(builder, offset.X, offset.Y) builder.WriteString(string(Styles.Reset)) } diff --git a/internal/client/client_utils.go b/internal/client/client_utils.go index 86a7057..4dcb811 100644 --- a/internal/client/client_utils.go +++ b/internal/client/client_utils.go @@ -7,6 +7,8 @@ import ( "log/slog" "sshpong/internal/lobby" "strings" + + "github.com/google/uuid" ) type InterrupterMessage struct { @@ -16,8 +18,8 @@ type InterrupterMessage struct { var help = fmt.Errorf("use invite 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 normal = "\033[0m" +var Red = "\x1b[31m" +var Normal = "\033[0m" func HandleUserInput(args []string, username string) ([]byte, error) { if len(args) == 0 { @@ -26,11 +28,15 @@ func HandleUserInput(args []string, username string) ([]byte, error) { switch args[0] { case "invite": if args[1] != "" { - msg, err := lobby.Marshal(lobby.InviteData{From: username, To: args[1]}, lobby.Invite) - if err != nil { - slog.Debug("invite message was not properly marshalled", "error", err) + 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) + if err != nil { + slog.Debug("invite message was not properly marshalled", "error", err) + } + return msg, err } - return msg, err } else { fmt.Println("Please provide a player to invite ") } @@ -84,13 +90,15 @@ func HandleInterruptInput(incoming InterrupterMessage, args []string, username s switch incoming.InterruptType { // Respond with yes if you accept game case "invite": + slog.Debug("handling invite interrupt") if len(args) < 1 { return []byte{}, nil } else { if strings.ToLower(args[0]) == "y" || strings.ToLower(args[0]) == "yes" { msg, err := lobby.Marshal(lobby.AcceptData{ - From: username, - To: incoming.Content, + From: username, + To: incoming.Content, + GameID: uuid.NewString(), }, lobby.Accept) if err != nil { 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 - case "accepted": - msg, err := lobby.Marshal(lobby.DisconnectData{ - From: incoming.Content, - }, lobby.Disconnect) - if err != nil { - slog.Debug("disconnect message was not properly marshalled", "error", err) - } - return msg, err + // TODO: Do we need this accepted? Disconnect and connect to game + // case "accepted": + // msg, err := lobby.Marshal(lobby.DisconnectData{ + // From: incoming.Content, + // }, lobby.Disconnect) + // if err != nil { + // slog.Debug("disconnect message was not properly marshalled", "error", err) + // } + // return msg, err + case "start_game": msg, err := lobby.Marshal(lobby.StartGameData{ To: "", GameID: incoming.Content, - }, lobby.Chat) + }, lobby.StartGame) if err != nil { 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) { header := msg[0] - switch header { case lobby.Invite: imsg, err := lobby.Unmarshal[lobby.InviteData](msg) @@ -160,6 +168,7 @@ func HandleServerMessage(msg []byte) (InterrupterMessage, error) { if err != nil { return InterrupterMessage{}, errors.New("Not a properly formatted start game message") } + fmt.Println("Your invite was accepted. Press Enter to join game") return InterrupterMessage{ InterruptType: "start_game", Content: sgmsg.GameID, @@ -206,7 +215,7 @@ func HandleServerMessage(msg []byte) (InterrupterMessage, error) { if err != nil { 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 diff --git a/internal/client/game.go b/internal/client/game.go index 51b5667..7728e5a 100644 --- a/internal/client/game.go +++ b/internal/client/game.go @@ -166,9 +166,9 @@ func handleGameInput(bytes []byte) { // Up case 'w': 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{ - X: 0, Y: state.Player1.Pos.Y, + X: -50, Y: state.Player1.Pos.Y, }) if err != nil { slog.Debug("error marshalling player movement", slog.Any("error", err)) @@ -180,9 +180,9 @@ func handleGameInput(bytes []byte) { egress <- update } 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{ - X: 0, Y: state.Player2.Pos.Y, + X: 50, Y: state.Player2.Pos.Y, }) if err != nil { slog.Debug("error marshalling Player2 movement", slog.Any("error", err)) @@ -197,9 +197,9 @@ func handleGameInput(bytes []byte) { // Down case 's': 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{ - X: 0, Y: state.Player1.Pos.Y, + X: -50, Y: state.Player1.Pos.Y, }) if err != nil { slog.Debug("error marshalling player movement", slog.Any("error", err)) @@ -211,9 +211,9 @@ func handleGameInput(bytes []byte) { egress <- update } 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{ - X: 0, Y: state.Player2.Pos.Y, + X: 50, Y: state.Player2.Pos.Y, }) if err != nil { slog.Debug("error marshalling player movement", slog.Any("error", err)) @@ -252,9 +252,9 @@ func handleGameInput(bytes []byte) { // Up case 65: 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{ - X: 0, Y: state.Player1.Pos.Y, + X: -50, Y: state.Player1.Pos.Y, }) if err != nil { slog.Debug("error marshalling player movement", slog.Any("error", err)) @@ -266,9 +266,9 @@ func handleGameInput(bytes []byte) { egress <- update } 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{ - X: 0, Y: state.Player2.Pos.Y, + X: 50, Y: state.Player2.Pos.Y, }) if err != nil { slog.Debug("error marshalling Player2 movement", slog.Any("error", err)) @@ -283,9 +283,9 @@ func handleGameInput(bytes []byte) { // Down case 66: 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{ - X: 0, Y: state.Player1.Pos.Y, + X: -50, Y: state.Player1.Pos.Y, }) if err != nil { slog.Debug("error marshalling player movement", slog.Any("error", err)) @@ -297,9 +297,9 @@ func handleGameInput(bytes []byte) { egress <- update } 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{ - X: 0, Y: state.Player2.Pos.Y, + X: 50, Y: state.Player2.Pos.Y, }) if err != nil { slog.Debug("error marshalling player movement", slog.Any("error", err)) diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..0882c61 --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/lobby/lobby.go b/internal/lobby/lobby.go index a92d328..1429140 100644 --- a/internal/lobby/lobby.go +++ b/internal/lobby/lobby.go @@ -6,8 +6,6 @@ import ( "log/slog" "net" "sync" - - "github.com/google/uuid" ) type Lobby struct { @@ -37,6 +35,7 @@ func CreateLobby() *Lobby { go func(lm *sync.Map) { for { msg := <-externalMessageChan + slog.Debug("forwarding external message") tc, ok := lm.Load(msg.Target) if !ok { @@ -170,10 +169,17 @@ func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) { return []byte{}, err } - l.ExternalMessageChannel <- ExternalMessage{ - From: i.From, - Target: i.To, - Message: msg, + _, 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{ + From: i.From, + Target: i.To, + Message: msg, + } } return Marshal(PendingInviteData{ @@ -195,12 +201,11 @@ func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) { return []byte{}, err } - gID := uuid.NewString() + gID := a.GameID - msg, err := Marshal(AcceptedData{ - Accepter: a.From, - GameID: gID, - }, Accepted) + msg, err := Marshal(StartGameData{ + GameID: gID, + }, StartGame) l.ExternalMessageChannel <- ExternalMessage{ From: a.From, @@ -208,23 +213,25 @@ func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) { Message: msg, } + slog.Debug("Sent start game message to inviter") + return Marshal(StartGameData{ To: a.From, + From: a.To, GameID: gID, }, 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. - return Marshal(StartGameData{ - To: "", - GameID: a.GameID, - }, StartGame) + // TODO: figure out the accepted and start game data situation... To field is a little hard to fill. + // case Accepted: + // a, err := Unmarshal[AcceptedData](msg) + // if err != nil { + // 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 // case StartGame: diff --git a/internal/lobby/lobby_messages.go b/internal/lobby/lobby_messages.go index 5256660..1ebe64d 100644 --- a/internal/lobby/lobby_messages.go +++ b/internal/lobby/lobby_messages.go @@ -36,18 +36,20 @@ type PendingInviteData struct { } type AcceptData struct { - From string `json:"from"` - To string `json:"to"` + From string `json:"from"` + To string `json:"to"` + GameID string `json:"gameID"` } type AcceptedData struct { Accepter string `json:"accepter"` - GameID string `json:"game_id"` + GameID string `json:"gameID"` } type StartGameData struct { To string `json:"to"` - GameID string `json:"game_id"` + From string `json:"from"` + GameID string `json:"gameID"` } type DeclineData struct { diff --git a/internal/pong/pong.go b/internal/pong/pong.go index f7f3f39..da8fcb3 100644 --- a/internal/pong/pong.go +++ b/internal/pong/pong.go @@ -23,7 +23,7 @@ var player2 GameClient var ingress chan StateUpdate var egress chan StateUpdate -const posXBound = 100 +const posXBound = 52 const negXBound = posXBound * -1 const posYBound = 50 const negYBound = posYBound * -1 @@ -141,6 +141,9 @@ func gameLoop(state *GameState) { return } ingress <- msg + if msg.FieldPath == "Winner" { + return + } } }() @@ -157,6 +160,9 @@ func gameLoop(state *GameState) { return } ingress <- msg + if msg.FieldPath == "Winner" { + return + } } }() @@ -164,6 +170,9 @@ func gameLoop(state *GameState) { for { msg := <-egress broadcastUpdate(msg) + if msg.FieldPath == "Winner" { + return + } } }() @@ -176,17 +185,23 @@ func gameLoop(state *GameState) { if err != nil { fmt.Println("FUCK!~", err) } + if msg.FieldPath == "Winner" { + slog.Debug("Closing game loop on winner message") + return + } case _ = <-ticker.C: update := process(state) egress <- update + if update.FieldPath == "Winner" { + slog.Debug("Closing game loop") + return + } } - } } func process(state *GameState) StateUpdate { - // Move players // Check if player edge is out of bounds // 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 { // 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 { + slog.Debug("Player1 paddle hit!") state.Ball.Pos.X = (negXBound + 1) - (state.Ball.Pos.X - (negXBound + 1)) state.Ball.Vel.X = state.Ball.Vel.X * -1.001 - angleTweak := (state.Ball.Pos.Y - state.Player2.Pos.Y) / (state.Player2.Size.Y / 2) - state.Ball.Vel.Y = state.Ball.Vel.Y * angleTweak + angleTweak := (state.Ball.Pos.Y - state.Player1.Pos.Y) / (state.Player1.Size.Y / 2) + state.Ball.Vel.Y = angleTweak / 10 } else { + slog.Debug("Player1 paddle miss...") state.Ball.Pos.X = 0 state.Ball.Pos.Y = 0 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 { // 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 { + slog.Debug("Player2 paddle hit!") state.Ball.Pos.X = (posXBound - 1) - (state.Ball.Pos.X - (posXBound - 1)) state.Ball.Vel.X = state.Ball.Vel.X * -1.001 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 { + slog.Debug("Player2 paddle miss...") state.Ball.Pos.X = 0 state.Ball.Pos.Y = 0 state.Ball.Vel.X = -1 diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index b015fc0..3929461 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -6,7 +6,8 @@ import ( "sshpong/internal/ansii" "sshpong/internal/pong" "strings" - "time" + + "golang.org/x/term" ) var ( @@ -15,96 +16,88 @@ var ( millisecondTimeFrame float64 = float64(1 / targetFpMilli) quit chan bool 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) { - // drawScreen(state) - fmt.Print("\033c") - fmt.Println("Player 1", state.Player1.Pos.X, state.Player1.Pos.Y) - 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 + // fmt.Println("Player 1", ((state.Player1.Pos.X+50)/100)*width, ((state.Player1.Pos.Y+50)/100)*height) + // fmt.Println("Player 2", ((state.Player2.Pos.X+50)/100)*width, ((state.Player2.Pos.Y+50)/100)*height) + // fmt.Println("Ball", ((state.Ball.Pos.X+50)/100)*width, ((state.Ball.Pos.Y+50)/100)*height) var builder = strings.Builder{} 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) - ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player1.Pos.X), Y: int(state.Player1.Pos.Y)}, ansii.Colors.Purple) - 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) - ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player2.Pos.X), Y: int(state.Player2.Pos.Y)}, ansii.Colors.Purple) - ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player2.Pos.X), Y: int(state.Player2.Pos.Y) + 5}, ansii.Colors.Purple) - // Quit instructions - // builder.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: 0, Y: height}))) - // builder.WriteString("q to quit") + x1, y1 := transformToTermPos(state.Player1.Pos) + builder.WriteString(renderBox(&builder, x1+2, y1, 2, 10, cyan)) + + x2, y2 := transformToTermPos(state.Player2.Pos) + builder.WriteString(renderBox(&builder, x2, y2, 2, 10, purple)) + + xb, yb := transformToTermPos(state.Ball.Pos) + builder.WriteString(renderPixel(&builder, xb, yb, red)) + + builder.WriteString(renderMessage(&builder, state.Message)) os.Stdout.WriteString(builder.String()) } -func drawFrameStats(frameNum int, frameTimeMs float64) { - width, height := ansii.GetTermSize() - 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 setCursorPos(x, y int) string { + return fmt.Sprintf("\033[%d;%dH", y, x) } -func handleInput(rawInput rune) { - action := ProcessInput(rawInput) - width, height := ansii.GetTermSize() - - switch action { - case Quit: - 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) - } -} - -func waitForFpsLock(startMs float64) { - for { - var nowMs = float64(time.Now().UnixNano()) / 1_000_000.0 - if nowMs-startMs >= millisecondTimeFrame { - break +// Renders a box with center positioned at X,Y with specified width and height +func renderBox(builder *strings.Builder, X, Y, width, height int, style string) string { + str := "" + for x := X - (width / 2); x < X+(width/2); x++ { + for y := Y - (height / 2); y < Y+(height/2); y++ { + str = str + (setCursorPos(x, y) + style + "█") } } + + return str +} + +func renderPixel(builder *strings.Builder, x, y int, style string) string { + return (setCursorPos(x, y) + style + "█") +} + +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 } diff --git a/invite_proc.txt b/invite_proc.txt new file mode 100644 index 0000000..9350379 --- /dev/null +++ b/invite_proc.txt @@ -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 diff --git a/makefile b/makefile index e43f730..b431301 100644 --- a/makefile +++ b/makefile @@ -1,2 +1,13 @@ lint: 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 + diff --git a/tmp/build-errors.log b/tmp/build-errors.log new file mode 100644 index 0000000..8a8172f --- /dev/null +++ b/tmp/build-errors.log @@ -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 \ No newline at end of file diff --git a/tmp/client/main b/tmp/client/main new file mode 100755 index 0000000..a236cab Binary files /dev/null and b/tmp/client/main differ diff --git a/tmp/server/main b/tmp/server/main new file mode 100755 index 0000000..9b012fe Binary files /dev/null and b/tmp/server/main differ