This commit is contained in:
2024-10-03 22:41:33 -06:00
parent 15e7f20f1a
commit 067d22f3a9
20 changed files with 476 additions and 257 deletions
+21 -16
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))
}
+29 -20
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] != "" {
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
+16 -16
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
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
}
+30 -23
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,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:
+6 -4
View File
@@ -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 {
+25 -6
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
+73 -80
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)
}
}
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
}