Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

on:
push:
branches:
- main
- master
pull_request:

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Install native dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libc6-dev \
libglu1-mesa-dev \
libgl1-mesa-dev \
libxcursor-dev \
libxi-dev \
libxinerama-dev \
libxrandr-dev \
libxxf86vm-dev \
libasound2-dev \
pkg-config

- name: Verify formatting
run: test -z "$(gofmt -l main.go game/*.go)"

- name: Run tests
run: make test
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.PHONY: help run build test tidy fmt

help:
@echo "Available targets:"
@echo " make run - Run the game"
@echo " make build - Build the gong binary"
@echo " make test - Run Go tests"
@echo " make tidy - Tidy go modules"
@echo " make fmt - Format Go source files"

run:
go run .

build:
go build ./...

test:
go test ./...

tidy:
go mod tidy

fmt:
gofmt -w main.go game/*.go
49 changes: 30 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,48 @@
# Gong! - The Go Pong

Retro styled classic [Pong](https://en.wikipedia.org/wiki/Pong) clone based upon [Ebiten](https://ebiten.org/) game framework.
Retro styled classic [Pong](https://en.wikipedia.org/wiki/Pong) clone based on the [Ebitengine](https://ebitengine.org/) game framework.

With go version 16 or above installed, type
## Run from source

With Go **1.22+** installed, run:

```bash
git clone http://github.com/jenska/gong
git clone https://github.com/jenska/gong.git
cd gong
go run main.go
go run .
```

to start

![Screenshot](game/assets/screenshot.png)

For a standalone gong game, type
## Install

To build and install a standalone `gong` binary into your Go bin path:

```bash
go install
```

## Makefile targets

Common development tasks are available through `make`:

```bash
make run
make build
make test
make fmt
make tidy
```

## Controls

- `W` / `S`: move left paddle
- `↑` / `↓`: move right paddle
- `Esc`: quit

## Dependencies

Ebiten requires some additional libs installed
Ebitengine requires additional native libraries on Linux.

### Debian / Ubuntu

Expand All @@ -36,15 +56,6 @@ sudo apt install libc6-dev libglu1-mesa-dev libgl1-mesa-dev libxcursor-dev libxi
sudo dnf install mesa-libGLU-devel mesa-libGLES-devel libXrandr-devel libXcursor-devel libXinerama-devel libXi-devel libXxf86vm-devel alsa-lib-devel pkg-config
```

## Thanks to

[OpenGameArt.Org](https://opengameart.org/)

## Soon to come
## Thanks

- ~~update package structure~~
- ~~stereo sounds~~
- ~~make window resizable~~
- ~~upgrade to ebiten 2.0~~
- ~~embedded assets~~
- AI support with Tensorflow. Soon to come!
- [OpenGameArt.org](https://opengameart.org/)
120 changes: 108 additions & 12 deletions game/paddle.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package game

import (
"math"
"math/rand"
"time"

ebiten "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
Expand All @@ -10,6 +14,13 @@ const (
paddleHeight = 80
paddleShift = 50
paddleAcceleration = 1

aiMaxSpeed = 6.0
aiDeadZone = 6.0
aiAimError = 18.0
aiIdleAimError = 10.0
aiReactionMin = 4
aiReactionJitter = 6
)

type paddle struct {
Expand All @@ -19,6 +30,10 @@ type paddle struct {
isComputer *bool
score *int
upKey, downKey ebiten.Key

aiTargetY float64
aiCooldown int
aiRandom *rand.Rand
}

func newPaddle(player int, score *int, mode *bool) *paddle {
Expand All @@ -36,6 +51,8 @@ func newPaddle(player int, score *int, mode *bool) *paddle {
p.downKey = ebiten.KeyDown
}
p.y = windowHeight/2 - paddleHeight/2
p.aiTargetY = p.y
p.aiRandom = rand.New(rand.NewSource(time.Now().UnixNano() + int64(player*101)))
p.image = ebiten.NewImage(paddleWidth, paddleHeight)
p.image.Fill(objectColor)
p.ghostImage = ebiten.NewImage(paddleWidth, paddleHeight)
Expand All @@ -46,27 +63,25 @@ func newPaddle(player int, score *int, mode *bool) *paddle {
func (p *paddle) update(g *Gong) {
if g.state == play {
if *p.isComputer {
p.y = g.ball.y - paddleHeight/2
p.updateComputer(g)
} else {
if inpututil.KeyPressDuration(p.upKey) > 0 {
p.yVelocity -= paddleAcceleration
}
if inpututil.KeyPressDuration(p.downKey) > 0 {
p.yVelocity += paddleAcceleration
}

p.y += p.yVelocity
}

if p.y < 0 {
p.y = 1.0
p.yVelocity = 0
playSound(ping)
} else if p.y+paddleHeight > windowHeight {
p.y = windowHeight - paddleHeight - 1.0
p.yVelocity = 0
playSound(ping)
}

if p.y < 0 {
p.y = 1.0
p.yVelocity = 0
playSound(ping)
} else if p.y+paddleHeight > windowHeight {
p.y = windowHeight - paddleHeight - 1.0
p.yVelocity = 0
playSound(ping)
}

// inelastic collision
Expand Down Expand Up @@ -99,3 +114,84 @@ func (p *paddle) update(g *Gong) {
}
p.visible = g.state == play || g.state == interrupt
}

func (p *paddle) updateComputer(g *Gong) {
if p.aiCooldown > 0 {
p.aiCooldown--
}

if p.aiCooldown == 0 {
p.aiTargetY = p.nextAITargetY(g)
p.aiCooldown = aiReactionMin + p.aiRandom.Intn(aiReactionJitter+1)
}

delta := p.aiTargetY - p.y
if math.Abs(delta) < aiDeadZone {
p.yVelocity *= 0.7
p.y += p.yVelocity
return
}

p.yVelocity = clamp(delta*0.20, -aiMaxSpeed, aiMaxSpeed)
p.y += p.yVelocity
}

func (p *paddle) nextAITargetY(g *Gong) float64 {
if p.isBallApproaching(g.ball) {
return p.predictBallY(g.ball) - paddleHeight/2 + p.randomError(aiAimError)
}
idleTarget := float64(windowHeight/2-paddleHeight/2) + p.randomError(aiIdleAimError)
return idleTarget
}

func (p *paddle) isBallApproaching(b *ball) bool {
if p.player == leftPlayer {
return b.xVelocity < 0
}
return b.xVelocity > 0
}

func (p *paddle) predictBallY(b *ball) float64 {
targetX := p.x + paddleWidth/2
if p.player == leftPlayer {
targetX += ballRadius
} else {
targetX -= ballRadius
}
Comment on lines +155 to +160

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mirror right-paddle X target in AI prediction

For the right-side computer paddle, targetX resolves to p.x here, but the actual contact/reset position for right-paddle hits in update is p.x - paddleWidth/2 - ballRadius. That 20px mismatch makes timeToReach too large, so predicted Y is systematically off; in AI-vs-AI mode this causes avoidable misses when the ball has meaningful vertical speed.

Useful? React with 👍 / 👎.


if b.xVelocity == 0 {
return b.y
}
timeToReach := (targetX - b.x) / b.xVelocity
if timeToReach <= 0 {
return b.y
}

predictedY := b.y + b.yVelocity*timeToReach
minY := float64(ballRadius)
maxY := float64(windowHeight - ballRadius)

for predictedY < minY || predictedY > maxY {
if predictedY < minY {
predictedY = minY + (minY - predictedY)
} else {
predictedY = maxY - (predictedY - maxY)
}
}

return predictedY
}

func (p *paddle) randomError(max float64) float64 {
return (p.aiRandom.Float64()*2 - 1) * max
}

func clamp(v, min, max float64) float64 {
if v < min {
return min
}
if v > max {
return max
}
return v
}
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
module github.com/jenska/gong

go 1.16
go 1.22

require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/hajimehoshi/ebiten/v2 v2.0.6
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
)

require (
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2 // indirect
github.com/hajimehoshi/oto v0.6.8 // indirect
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/mobile v0.0.0-20210208171126-f462b3930c8f // indirect
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 // indirect
)
Loading