summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2024-08-25 12:05:01 -0700
committerRose Hogenson <rosehogenson@posteo.net>2024-08-25 12:05:01 -0700
commit827f639912529589e651da5c35e28881c8e08c1f (patch)
treedbca88c4c3749c7286f54d326c81ad9f970734f6
parenta2e1ec9472d7ff1d3387dc9d059dcee019a84996 (diff)
downloadpongo-827f639912529589e651da5c35e28881c8e08c1f.tar.zst
Port to SDL.
-rw-r--r--flake.nix14
-rw-r--r--go.mod8
-rw-r--r--go.sum10
-rw-r--r--numbers/numbers.go88
-rw-r--r--ping.mp3bin4016 -> 0 bytes
-rw-r--r--ping.wavbin0 -> 36044 bytes
-rw-r--r--pongo.go240
7 files changed, 247 insertions, 113 deletions
diff --git a/flake.nix b/flake.nix
index b415c15..dcbd70d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -22,7 +22,7 @@
# Nixpkgs instantiated for supported system types.
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
- deps = pkgs: with pkgs; [ libGL xorg.libXi xorg.libXcursor xorg.libXrandr xorg.libXinerama wayland libxkbcommon ];
+ deps = pkgs: with pkgs; [ SDL2_mixer ];
in
{
@@ -40,13 +40,13 @@
# in the Nix store.
src = ./.;
- vendorHash = "sha256-ZWMZkwDMSUuuWCInVJ1YTd0W5GJedGrxYugKv1jhx2U=";
+ vendorHash = "sha256-0GaAPAg940MKg4XOw+WqZBT8jT2gXJIAWsltcHMgzfw=";
+ nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = deps pkgs;
- proxyVendor = true;
- postFixup = ''
- patchelf $out/bin/pongo --add-rpath ${pkgs.lib.makeLibraryPath [ pkgs.alsa-lib ]}
+ preBuild = ''
+ export CGO_CFLAGS=$(pkg-config --cflags --libs SDL2_mixer)
'';
};
});
@@ -58,9 +58,9 @@
in
{
default = pkgs.mkShell {
- nativeBuildInputs = with pkgs; [ go gopls gotools go-tools ] ++ deps pkgs;
+ nativeBuildInputs = with pkgs; [ go gopls gotools go-tools pkg-config ] ++ deps pkgs;
shellHook = ''
- export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath [ pkgs.alsa-lib ]}
+ export CGO_CFLAGS=$(pkg-config --cflags --libs SDL2_mixer)
'';
};
});
diff --git a/go.mod b/go.mod
index f1e3ac0..a44c486 100644
--- a/go.mod
+++ b/go.mod
@@ -2,10 +2,4 @@ module gitlab.com/rhogenson/pongo
go 1.22
-require github.com/gen2brain/raylib-go/raylib v0.0.0-20240807111636-8861ee437da9
-
-require (
- github.com/ebitengine/purego v0.7.1 // indirect
- golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
- golang.org/x/sys v0.20.0 // indirect
-)
+require github.com/veandco/go-sdl2 v0.4.40
diff --git a/go.sum b/go.sum
index 7e6902d..bc27727 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,2 @@
-github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
-github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
-github.com/gen2brain/raylib-go/raylib v0.0.0-20240807111636-8861ee437da9 h1:voUyZVwDxeiKv31gHmzHY95Oq/+0+ozj8zmKDiaag/o=
-github.com/gen2brain/raylib-go/raylib v0.0.0-20240807111636-8861ee437da9/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
-golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
-golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
-golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U=
+github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
diff --git a/numbers/numbers.go b/numbers/numbers.go
new file mode 100644
index 0000000..21e5cb5
--- /dev/null
+++ b/numbers/numbers.go
@@ -0,0 +1,88 @@
+package numbers
+
+import (
+ "github.com/veandco/go-sdl2/sdl"
+)
+
+const width = 60
+
+func drawDigit(surface *sdl.Surface, d int, x, y int32) error {
+ const (
+ height = 100
+ thickness = 10
+ )
+ white := sdl.MapRGB(surface.Format, 245, 245, 245)
+ switch d {
+ case 0:
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y, W: thickness, H: height}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: thickness, H: height}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height - thickness, W: width, H: thickness}, white)
+ case 1:
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y, W: thickness, H: height}, white)
+ case 2:
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y, W: thickness, H: height / 2}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height/2 - thickness/2, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height/2, W: thickness, H: height / 2}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height - thickness, W: width, H: thickness}, white)
+ case 3:
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y, W: thickness, H: height}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height/2 - thickness/2, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height - thickness, W: width, H: thickness}, white)
+ case 4:
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: thickness, H: height / 2}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height/2 - thickness/2, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y, W: thickness, H: height}, white)
+ case 5:
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: thickness, H: height / 2}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height/2 - thickness/2, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y + height/2, W: thickness, H: height / 2}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height - thickness, W: width, H: thickness}, white)
+ case 6:
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: thickness, H: height}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height/2 - thickness/2, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y + height/2, W: thickness, H: height / 2}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height - thickness, W: width, H: thickness}, white)
+ case 7:
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y, W: thickness, H: height}, white)
+ case 8:
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: thickness, H: height}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y, W: thickness, H: height}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height/2 - thickness/2, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height - thickness, W: width, H: thickness}, white)
+ case 9:
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: thickness, H: height / 2}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x + width - thickness, Y: y, W: thickness, H: height}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height/2 - thickness/2, W: width, H: thickness}, white)
+ surface.FillRect(&sdl.Rect{X: x, Y: y + height - thickness, W: width, H: thickness}, white)
+ default:
+ panic("invalid digit")
+ }
+ return nil
+}
+
+func Draw(surface *sdl.Surface, n int, x, y int32) error {
+ const padding = 20
+
+ if n == 0 {
+ return drawDigit(surface, 0, x, y)
+ }
+ digits := make([]int, 0, 19)
+ for ; n > 0; n /= 10 {
+ digits = append(digits, n%10)
+ }
+ for i := len(digits) - 1; i >= 0; i-- {
+ if err := drawDigit(surface, digits[i], x, y); err != nil {
+ return err
+ }
+ x += width + padding
+ }
+ return nil
+}
diff --git a/ping.mp3 b/ping.mp3
deleted file mode 100644
index 87145b8..0000000
--- a/ping.mp3
+++ /dev/null
Binary files differ
diff --git a/ping.wav b/ping.wav
new file mode 100644
index 0000000..8ea7d00
--- /dev/null
+++ b/ping.wav
Binary files differ
diff --git a/pongo.go b/pongo.go
index f4dc038..b632626 100644
--- a/pongo.go
+++ b/pongo.go
@@ -2,79 +2,76 @@ package main
import (
_ "embed"
- "github.com/gen2brain/raylib-go/raylib"
+ "fmt"
+ "github.com/veandco/go-sdl2/mix"
+ "github.com/veandco/go-sdl2/sdl"
+ "gitlab.com/rhogenson/pongo/numbers"
"math"
- "strconv"
+ "os"
+ "time"
)
const (
screenWidth = 1920
screenHeight = 1080
- ballSpeed = 5
-
+ ballSpeed = 5
paddleSpeed = 5
)
-func printCenter(text string, size int32, color rl.Color) {
- rl.DrawText(text, screenWidth/2-rl.MeasureText(text, size), screenHeight/2, size, color)
-}
-
-type rect rl.Rectangle
-
-func (r rect) bottom() float32 {
- return r.Y + r.Height
+func rectRight(r *sdl.Rect) int32 {
+ return r.X + r.W
}
-func (r rect) right() float32 {
- return r.X + r.Width
+func rectBottom(r *sdl.Rect) int32 {
+ return r.Y + r.H
}
-func (r rect) collides(s rect) bool {
- return rl.CheckCollisionRecs(rl.Rectangle(r), rl.Rectangle(s))
+type vector2 struct {
+ x, y float64
}
-func (r rect) draw(color rl.Color) {
- rl.DrawRectangleRec(rl.Rectangle(r), color)
+func (v vector2) add(w vector2) vector2 {
+ return vector2{v.x + w.x, v.y + w.y}
}
type ball struct {
- pos rl.Vector2
- velocity rl.Vector2
+ pos vector2
+ velocity vector2
}
-func (b *ball) bounds() rect {
+func (b *ball) bounds() *sdl.Rect {
const ballSize = 30
- return rect{
- X: b.pos.X,
- Y: b.pos.Y,
- Width: ballSize,
- Height: ballSize,
+ return &sdl.Rect{
+ X: int32(b.pos.x - ballSize/2),
+ Y: int32(b.pos.y - ballSize/2),
+ W: ballSize,
+ H: ballSize,
}
}
-func (b *ball) predictIntersection(targetX float32) (float32, bool) {
- if b.velocity.X <= 0 {
+func (b *ball) predictIntersection(targetX float64) (float64, bool) {
+ if b.velocity.x <= 0 {
return 0, false
}
- x := b.pos.X
- y := b.pos.Y
+ x := b.pos.x
+ y := b.pos.y
velocity := b.velocity
dist := targetX - x
- prediction := y + velocity.Y*dist/velocity.X
+ prediction := y + velocity.y*dist/velocity.x
if prediction < 0 {
bounces := int(-prediction / screenHeight)
if bounces%2 == 0 {
- return -prediction - float32(screenHeight*bounces), true
+ return -prediction - float64(screenHeight*bounces), true
}
- return prediction + float32(screenHeight*(bounces+1)), true
+ return prediction + float64(screenHeight*(bounces+1)), true
}
if prediction > screenHeight {
bounces := int(prediction / screenHeight)
if bounces%2 == 0 {
- return prediction - float32(screenHeight*bounces), true
+ return prediction - float64(screenHeight*bounces), true
}
- return -prediction + float32(screenHeight*(bounces+1)), true
+ return -prediction + float64(screenHeight*(bounces+1)), true
}
return prediction, true
}
@@ -87,29 +84,29 @@ const (
)
func (b *ball) reset(side side) {
- b.pos = rl.Vector2{X: screenWidth / 2, Y: screenHeight / 2}
- b.velocity = rl.Vector2{X: ballSpeed}
+ b.pos = vector2{screenWidth / 2, screenHeight / 2}
+ b.velocity = vector2{x: ballSpeed}
if side == left {
- b.velocity.X = -b.velocity.X
+ b.velocity.x = -b.velocity.x
}
}
type paddle struct {
- pos float32
+ pos float64
side side
}
-func (p *paddle) bounds() rect {
+func (p *paddle) bounds() *sdl.Rect {
const (
paddleHeight = 200
thickness = 10
padding = 100
)
- r := rect{
- X: padding - thickness/2,
- Y: p.pos - paddleHeight/2,
- Width: thickness,
- Height: paddleHeight,
+ r := &sdl.Rect{
+ X: padding - thickness/2,
+ Y: int32(p.pos - paddleHeight/2),
+ W: thickness,
+ H: paddleHeight,
}
if p.side == right {
r.X = screenWidth - padding - thickness/2
@@ -117,21 +114,35 @@ func (p *paddle) bounds() rect {
return r
}
-//go:embed ping.mp3
-var pingMP3 []byte
+//go:embed ping.wav
+var pingWAV []byte
-func main() {
- rl.InitWindow(screenWidth, screenHeight, "pongo")
- defer rl.CloseWindow()
- rl.SetTargetFPS(120)
+func pong() error {
+ const fps = 60
- rl.InitAudioDevice()
- defer rl.CloseAudioDevice()
+ if err := sdl.Init(sdl.INIT_AUDIO & sdl.INIT_VIDEO & sdl.INIT_EVENTS); err != nil {
+ return err
+ }
+ defer sdl.Quit()
+ if err := mix.OpenAudio(44100, mix.DEFAULT_FORMAT, 2, 4096); err != nil {
+ return err
+ }
+ defer mix.CloseAudio()
- pingWave := rl.LoadWaveFromMemory(".mp3", pingMP3, int32(len(pingMP3)))
- defer rl.UnloadWave(pingWave)
- ping := rl.LoadSoundFromWave(pingWave)
- defer rl.UnloadSound(ping)
+ ping, err := mix.QuickLoadWAV(pingWAV)
+ if err != nil {
+ return err
+ }
+ defer ping.Free()
+
+ window, err := sdl.CreateWindow("pongo", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, screenWidth, screenHeight, sdl.WINDOW_SHOWN)
+ if err != nil {
+ return err
+ }
+ defer window.Destroy()
+
+ upPressed := false
+ downPressed := false
playerScore := 0
aiScore := 0
@@ -142,74 +153,121 @@ func main() {
paddle1 := paddle{pos: screenHeight / 2, side: left}
paddle2 := paddle{pos: screenHeight / 2, side: right}
- for !rl.WindowShouldClose() {
+ frameTargetTime := time.Now().Add(time.Second / fps)
+
+ for {
+ for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
+ switch t := event.(type) {
+ case *sdl.QuitEvent:
+ return nil
+ case *sdl.KeyboardEvent:
+ switch t.Keysym.Sym {
+ case sdl.K_UP:
+ upPressed = t.State == sdl.PRESSED
+ case sdl.K_DOWN:
+ downPressed = t.State == sdl.PRESSED
+ }
+ }
+ }
+
switch {
- case rl.IsKeyDown(rl.KeyUp) && paddle1.bounds().Y >= 0:
+ case upPressed && paddle1.bounds().Y >= 0:
paddle1.pos -= paddleSpeed
- case rl.IsKeyDown(rl.KeyDown) && paddle1.bounds().bottom() <= screenHeight:
+ case downPressed && rectBottom(paddle1.bounds()) <= screenHeight:
paddle1.pos += paddleSpeed
}
- if prediction, ok := ball.predictIntersection(paddle2.bounds().X); ok {
+ if prediction, ok := ball.predictIntersection(float64(paddle2.bounds().X)); ok {
switch {
- case paddle2.pos < prediction && paddle2.bounds().bottom() <= screenHeight:
+ case paddle2.pos < prediction && rectBottom(paddle2.bounds()) <= screenHeight:
paddle2.pos += paddleSpeed
case paddle2.pos > prediction && paddle2.bounds().Y >= 0:
paddle2.pos -= paddleSpeed
}
}
- ball.pos = rl.Vector2Add(ball.pos, ball.velocity)
+ ball.pos = ball.pos.add(ball.velocity)
switch {
- case ball.bounds().collides(paddle1.bounds()):
- rl.PlaySound(ping)
- d := ball.pos.Y - paddle1.pos
+ case ball.bounds().HasIntersection(paddle1.bounds()):
+ ping.Play(-1, 0)
+ d := ball.pos.y - paddle1.pos
if d < 0 {
d = -d
}
- angle := d / (paddle1.bounds().Height/2 + ball.bounds().Height) * (75 * 2 * math.Pi / 360)
- if ball.pos.Y < paddle1.pos {
+ angle := d / (float64(paddle1.bounds().H)/2 + float64(ball.bounds().H)) * (75 * 2 * math.Pi / 360)
+ if ball.pos.y < paddle1.pos {
angle = -angle
}
- speed := ballSpeed * (1 + 4*d/(paddle1.bounds().Height/2+ball.bounds().Height))
- ball.velocity = rl.Vector2{X: speed * float32(math.Cos(float64(angle))), Y: speed * float32(math.Sin(float64(angle)))}
- case ball.bounds().collides(paddle2.bounds()):
- rl.PlaySound(ping)
- d := ball.pos.Y - paddle2.pos
+ speed := ballSpeed * (1 + 4*d/(float64(paddle1.bounds().H)/2+float64(ball.bounds().H)))
+ ball.velocity = vector2{speed * math.Cos(angle), speed * math.Sin(angle)}
+ case ball.bounds().HasIntersection(paddle2.bounds()):
+ ping.Play(-1, 0)
+ d := ball.pos.y - paddle2.pos
if d < 0 {
d = -d
}
- angle := d / (paddle2.bounds().Height/2 + ball.bounds().Height) * (75 * 2 * math.Pi / 360)
- if ball.pos.Y < paddle2.pos {
+ angle := d / (float64(paddle2.bounds().H)/2 + float64(ball.bounds().H)) * (75 * 2 * math.Pi / 360)
+ if ball.pos.y < paddle2.pos {
angle = -angle
}
angle += math.Pi
- speed := ballSpeed * (1 + 4*d/(paddle2.bounds().Height/2+ball.bounds().Height))
- ball.velocity = rl.Vector2{X: speed * float32(math.Cos(float64(angle))), Y: speed * float32(math.Sin(float64(angle)))}
- case ball.bounds().right() <= paddle1.bounds().X:
+ speed := ballSpeed * (1 + 4*d/(float64(paddle2.bounds().H)/2+float64(ball.bounds().H)))
+ ball.velocity = vector2{speed * math.Cos(angle), speed * math.Sin(angle)}
+ case rectRight(ball.bounds()) <= paddle1.bounds().X:
aiScore++
ball.reset(right)
case ball.bounds().X >= paddle2.bounds().X:
playerScore++
ball.reset(left)
- case ball.bounds().Y <= 0 || ball.bounds().bottom() >= screenHeight:
- ball.velocity.Y = -ball.velocity.Y
- rl.PlaySound(ping)
+ case ball.bounds().Y <= 0 || rectBottom(ball.bounds()) >= screenHeight:
+ ping.Play(-1, 0)
+ ball.velocity.y = -ball.velocity.y
}
- rl.BeginDrawing()
+ surface, err := window.GetSurface()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ continue
+ }
- rl.ClearBackground(rl.Black)
- rl.DrawLine(screenWidth/2, 0, screenWidth/2, screenHeight, rl.Gray)
+ gray := sdl.MapRGB(surface.Format, 130, 130, 130)
+ offWhite := sdl.MapRGB(surface.Format, 245, 245, 245)
- paddle1.bounds().draw(rl.RayWhite)
- paddle2.bounds().draw(rl.RayWhite)
- ball.bounds().draw(rl.RayWhite)
+ if err := surface.FillRect(nil, 0); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ if err := surface.FillRect(&sdl.Rect{X: screenWidth / 2, Y: 0, W: 1, H: screenHeight}, gray); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
- const fontSize = 120
- rl.DrawText(strconv.Itoa(playerScore), int32(paddle1.bounds().X)+20, 30, fontSize, rl.RayWhite)
- rl.DrawText(strconv.Itoa(aiScore), int32(paddle2.bounds().X)-rl.MeasureText("9999", fontSize), 30, fontSize, rl.RayWhite)
+ if err := surface.FillRect(paddle1.bounds(), offWhite); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ if err := surface.FillRect(paddle2.bounds(), offWhite); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ if err := surface.FillRect(ball.bounds(), offWhite); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
- rl.EndDrawing()
+ if err := numbers.Draw(surface, playerScore, paddle1.bounds().X+20, 30); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ if err := numbers.Draw(surface, aiScore, paddle2.bounds().X-500, 30); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+
+ if err := window.UpdateSurface(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+
+ time.Sleep(time.Until(frameTargetTime))
+ frameTargetTime = frameTargetTime.Add(time.Second / fps)
+ }
+}
+
+func main() {
+ if err := pong(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
}
}