package main import ( _ "embed" "fmt" "github.com/veandco/go-sdl2/mix" "github.com/veandco/go-sdl2/sdl" "gitlab.com/rhogenson/pongo/numbers" "math" "os" "time" ) const ( screenWidth = 1920 screenHeight = 1080 ballSpeed = 5 paddleSpeed = 5 ) func rectRight(r *sdl.Rect) int32 { return r.X + r.W } func rectBottom(r *sdl.Rect) int32 { return r.Y + r.H } type vector2 struct { x, y float64 } func (v vector2) add(w vector2) vector2 { return vector2{v.x + w.x, v.y + w.y} } type ball struct { pos vector2 velocity vector2 } func (b *ball) bounds() *sdl.Rect { const ballSize = 30 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 float64) (float64, bool) { if b.velocity.x <= 0 { return 0, false } x := b.pos.x y := b.pos.y velocity := b.velocity dist := targetX - x prediction := y + velocity.y*dist/velocity.x if prediction < 0 { bounces := int(-prediction / screenHeight) if bounces%2 == 0 { return -prediction - float64(screenHeight*bounces), true } return prediction + float64(screenHeight*(bounces+1)), true } if prediction > screenHeight { bounces := int(prediction / screenHeight) if bounces%2 == 0 { return prediction - float64(screenHeight*bounces), true } return -prediction + float64(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 float64 side side } func (p *paddle) bounds() *sdl.Rect { const ( paddleHeight = 200 thickness = 10 padding = 100 ) 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 } return r } //go:embed ping.wav var pingWAV []byte func pong() error { const fps = 60 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() 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 var ball ball ball.reset(left) paddle1 := paddle{pos: screenHeight / 2, side: left} paddle2 := paddle{pos: screenHeight / 2, side: right} 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 upPressed && paddle1.bounds().Y >= 0: paddle1.pos -= paddleSpeed case downPressed && rectBottom(paddle1.bounds()) <= screenHeight: paddle1.pos += paddleSpeed } if prediction, ok := ball.predictIntersection(float64(paddle2.bounds().X)); ok { switch { case paddle2.pos < prediction && rectBottom(paddle2.bounds()) <= screenHeight: paddle2.pos += paddleSpeed case paddle2.pos > prediction && paddle2.bounds().Y >= 0: paddle2.pos -= paddleSpeed } } ball.pos = ball.pos.add(ball.velocity) switch { case ball.bounds().HasIntersection(paddle1.bounds()): ping.Play(-1, 0) d := ball.pos.y - paddle1.pos if d < 0 { d = -d } 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/(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 / (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/(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 || rectBottom(ball.bounds()) >= screenHeight: ping.Play(-1, 0) ball.velocity.y = -ball.velocity.y } surface, err := window.GetSurface() if err != nil { fmt.Fprintln(os.Stderr, err) continue } gray := sdl.MapRGB(surface.Format, 130, 130, 130) offWhite := sdl.MapRGB(surface.Format, 245, 245, 245) 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) } 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) } 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) } }