package main import ( _ "embed" "fmt" "github.com/veandco/go-sdl2/mix" "github.com/veandco/go-sdl2/sdl" "math" "os" "strconv" "time" ) const ( screenWidth = 1920 screenHeight = 1080 ballSpeed = 5 paddleSpeed = 5 ) // Imagine a digital clock. Do they still make those? var digitSegments = []uint8{0x77, 0x24, 0x5d, 0x6d, 0x2e, 0x6b, 0x7b, 0x25, 0x7f, 0x6f} func drawNumber(renderer *sdl.Renderer, n int, x, y int32) error { const ( width = 60 height = 100 thickness = 10 padding = 20 ) for _, d := range strconv.Itoa(n) { segments := digitSegments[d-'0'] for i := range 7 { if segments&(1< screenHeight { bounces := int(prediction / screenHeight) if bounces%2 == 0 { return prediction - float32(screenHeight*bounces), true } return -prediction + float32(screenHeight*(bounces+1)), true } return prediction, true } type side int const ( left side = iota right ) func (b *ball) reset(side side) { b.pos = vector2{screenWidth / 2, screenHeight / 2} b.velocity = vector2{x: ballSpeed} if side == left { b.velocity.x = -b.velocity.x } } type paddle struct { pos float32 side side } func (p *paddle) bounds() *sdl.FRect { const ( paddleHeight = 200 thickness = 10 padding = 100 ) r := &sdl.FRect{ X: padding - thickness/2, Y: p.pos - paddleHeight/2, W: thickness, H: paddleHeight, } if p.side == right { r.X = screenWidth - padding - thickness/2 } return r } //go:embed ping.wav var pingWAV []byte func pong() error { defer sdl.Quit() if err := sdl.Init(sdl.INIT_AUDIO | sdl.INIT_VIDEO); err != nil { return err } if err := mix.OpenAudio(44100, mix.DEFAULT_FORMAT, 2, 4096); err != nil { return err } defer mix.CloseAudio() ping, err := mix.QuickLoadWAV(pingWAV) if err != nil { return err } defer ping.Free() window, renderer, err := sdl.CreateWindowAndRenderer(screenWidth, screenHeight, sdl.WINDOW_BORDERLESS) if err != nil { return err } defer window.Destroy() defer renderer.Destroy() playerScore := 0 aiScore := 0 var ball ball ball.reset(left) paddle1 := paddle{pos: screenHeight / 2, side: left} paddle2 := paddle{pos: screenHeight / 2, side: right} lastFrameTime := time.Now() accumulator := time.Duration(0) renderRate := time.NewTicker(time.Second / 60) defer renderRate.Stop() for frameStart := range renderRate.C { 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_q, sdl.K_c, sdl.K_ESCAPE: return nil } } } accumulator += frameStart.Sub(lastFrameTime) lastFrameTime = frameStart const simulationSpeed = time.Second / 120 for ; accumulator >= simulationSpeed; accumulator -= simulationSpeed { switch { case (keyPressed(sdl.K_UP) || keyPressed(sdl.K_w)) && paddle1.bounds().Y >= 0: paddle1.pos -= paddleSpeed case (keyPressed(sdl.K_DOWN) || keyPressed(sdl.K_s)) && rectBottom(paddle1.bounds()) <= screenHeight: paddle1.pos += paddleSpeed } if prediction, ok := ball.predictIntersection(paddle2.bounds().X); ok { switch { case paddle2.pos <= prediction-paddleSpeed && rectBottom(paddle2.bounds()) <= screenHeight: paddle2.pos += paddleSpeed case paddle2.pos >= prediction+paddleSpeed && paddle2.bounds().Y >= 0: paddle2.pos -= paddleSpeed } } ball.pos = ball.pos.add(ball.velocity) switch { case ball.bounds().HasIntersection(paddle1.bounds()) && ball.velocity.x <= 0: if _, err := ping.Play(-1, 0); err != nil { fmt.Fprintln(os.Stderr, err) } d := (ball.pos.y - paddle1.pos) / (paddle1.bounds().H/2 + ball.bounds().H) if d < 0 { d = -d } angle := d * 75 * 2 * math.Pi / 360 if ball.pos.y < paddle1.pos { angle = -angle } speed := ballSpeed * (1 + 4*d) ball.velocity = vector2{speed * float32(math.Cos(float64(angle))), speed * float32(math.Sin(float64(angle)))} case ball.bounds().HasIntersection(paddle2.bounds()) && ball.velocity.x >= 0: if _, err := ping.Play(-1, 0); err != nil { fmt.Fprintln(os.Stderr, err) } d := (ball.pos.y - paddle2.pos) / (paddle2.bounds().H/2 + ball.bounds().H) if d < 0 { d = -d } angle := d * 75 * 2 * math.Pi / 360 if ball.pos.y < paddle2.pos { angle = -angle } angle += math.Pi speed := ballSpeed * (1 + 4*d) ball.velocity = vector2{speed * float32(math.Cos(float64(angle))), speed * float32(math.Sin(float64(angle)))} case rectRight(ball.bounds()) <= 0: aiScore++ ball.reset(right) case ball.bounds().X >= screenWidth: playerScore++ ball.reset(left) case ball.bounds().Y <= 0 && ball.velocity.y < 0 || rectBottom(ball.bounds()) >= screenHeight && ball.velocity.y > 0: if _, err := ping.Play(-1, 0); err != nil { fmt.Fprintln(os.Stderr, err) } ball.velocity.y = -ball.velocity.y } } if err := renderer.SetDrawColor(0, 0, 0, 255); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRect(nil); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.SetDrawColor(130, 130, 130, 255); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRect(&sdl.Rect{X: screenWidth / 2, Y: 0, W: 1, H: screenHeight}); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.SetDrawColor(245, 245, 245, 255); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRectF(paddle1.bounds()); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRectF(paddle2.bounds()); err != nil { fmt.Fprintln(os.Stderr, err) } if err := renderer.FillRectF(ball.bounds()); err != nil { fmt.Fprintln(os.Stderr, err) } if err := drawNumber(renderer, playerScore, 150, 30); err != nil { fmt.Fprintln(os.Stderr, err) } if err := drawNumber(renderer, aiScore, 1300, 30); err != nil { fmt.Fprintln(os.Stderr, err) } renderer.Present() } panic("unreachable") } func main() { if err := pong(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }