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"
"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)
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...")

View File

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

3
config.json Normal file
View File

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

7
go.mod
View File

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

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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=

View File

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

View File

@ -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 <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 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] != "" {
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
}
} else {
fmt.Println("Please provide a player to invite ")
}
@ -84,6 +90,7 @@ 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 {
@ -91,6 +98,7 @@ func HandleInterruptInput(incoming InterrupterMessage, args []string, username s
msg, err := lobby.Marshal(lobby.AcceptData{
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

View File

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

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"
"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,11 +169,18 @@ func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) {
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{
From: i.From,
Target: i.To,
Message: msg,
}
}
return Marshal(PendingInviteData{
Recipient: i.To,
@ -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,
msg, err := Marshal(StartGameData{
GameID: gID,
}, Accepted)
}, 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)
// 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:

View File

@ -38,16 +38,18 @@ type PendingInviteData struct {
type AcceptData struct {
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 {

View File

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

View File

@ -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)
// 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 + "█")
}
}
func waitForFpsLock(startMs float64) {
for {
var nowMs = float64(time.Now().UnixNano()) / 1_000_000.0
if nowMs-startMs >= millisecondTimeFrame {
break
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
}

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