Skip to content

Commit 8305003

Browse files
committed
✨ add snake game sample
0 parents  commit 8305003

File tree

11 files changed

+626
-0
lines changed

11 files changed

+626
-0
lines changed

.github/workflows/go.yml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Go
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
branches:
9+
- master
10+
jobs:
11+
build_and_test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Setup go-task
15+
uses: pnorton5432/setup-task@v1
16+
with:
17+
task-version: 3.29.1
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
- name: Setup Go
21+
uses: actions/setup-go@v5
22+
with:
23+
go-version: 'stable'
24+
check-latest: true
25+
- name: Task Build for mage
26+
run: task build-gg

.gitignore

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Output of the go coverage tool, specifically when used with LiteIDE
15+
*.out
16+
17+
# Dependency directories (remove the comment below to include it)
18+
# vendor/
19+
20+
# Go workspace file
21+
go.work
22+
go.work.sum
23+
24+
# env file
25+
.env
26+
bin
27+
gg
28+
mage

README.md

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# golang-sample-with-snake-game
2+
3+
This repository is learn how to use ebiten to build a snake game
4+
5+
## game logic
6+
7+
```golang
8+
package internal
9+
10+
import (
11+
"image/color"
12+
"math/rand"
13+
"time"
14+
15+
"github.com/hajimehoshi/ebiten/v2"
16+
"github.com/hajimehoshi/ebiten/v2/text/v2"
17+
"github.com/hajimehoshi/ebiten/v2/vector"
18+
)
19+
20+
var (
21+
dirUp = Point{x: 0, y: -1}
22+
dirDown = Point{x: 0, y: 1}
23+
dirLeft = Point{x: -1, y: 0}
24+
dirRight = Point{x: 1, y: 0}
25+
MplusFaceSource *text.GoTextFaceSource
26+
)
27+
28+
const (
29+
gameSpeed = time.Second / 6
30+
ScreenWidth = 640
31+
ScreenHeight = 480
32+
gridSize = 20
33+
)
34+
35+
type Point struct {
36+
x, y int
37+
}
38+
39+
type Game struct {
40+
snake []Point
41+
direction Point
42+
lastUpdate time.Time
43+
food Point
44+
randGenerator *rand.Rand
45+
gameOver bool
46+
}
47+
48+
func (g *Game) Update() error {
49+
if g.gameOver {
50+
return nil
51+
}
52+
// handle key
53+
if ebiten.IsKeyPressed(ebiten.KeyW) {
54+
g.direction = dirUp
55+
} else if ebiten.IsKeyPressed(ebiten.KeyS) {
56+
g.direction = dirDown
57+
} else if ebiten.IsKeyPressed(ebiten.KeyA) {
58+
g.direction = dirLeft
59+
} else if ebiten.IsKeyPressed(ebiten.KeyD) {
60+
g.direction = dirRight
61+
}
62+
// slow down
63+
if time.Since(g.lastUpdate) < gameSpeed {
64+
return nil
65+
}
66+
g.lastUpdate = time.Now()
67+
68+
g.updateSnake(&g.snake, g.direction)
69+
return nil
70+
}
71+
72+
// isBadCollision - check if snake is collision
73+
func (g Game) isBadCollision(
74+
newHead Point,
75+
snake []Point,
76+
) bool {
77+
// check if out of bound
78+
if newHead.x < 0 || newHead.y < 0 ||
79+
newHead.x >= ScreenWidth/gridSize || newHead.y >= ScreenHeight/gridSize {
80+
return true
81+
}
82+
// is newhead collision
83+
for _, snakeBody := range snake {
84+
if snakeBody == newHead {
85+
return true
86+
}
87+
}
88+
return false
89+
}
90+
91+
// updateSnake - update snake with direction
92+
func (g *Game) updateSnake(snake *[]Point, direction Point) {
93+
head := (*snake)[0]
94+
95+
newHead := Point{
96+
x: head.x + direction.x,
97+
y: head.y + direction.y,
98+
}
99+
100+
// check collision for snake
101+
if g.isBadCollision(newHead, *snake) {
102+
g.gameOver = true
103+
return
104+
}
105+
// check collision with food
106+
if newHead == g.food {
107+
*snake = append(
108+
[]Point{newHead},
109+
*snake...,
110+
)
111+
g.SpawnFood()
112+
} else {
113+
*snake = append(
114+
[]Point{newHead},
115+
(*snake)[:len(*snake)-1]...,
116+
)
117+
}
118+
119+
}
120+
121+
// drawGameOverText - draw game over text on screen
122+
func (g *Game) drawGameOverText(screen *ebiten.Image) {
123+
face := &text.GoTextFace{
124+
Source: MplusFaceSource,
125+
Size: 48,
126+
}
127+
title := "Game Over!"
128+
w, h := text.Measure(title,
129+
face,
130+
face.Size,
131+
)
132+
op := &text.DrawOptions{}
133+
op.GeoM.Translate(ScreenWidth/2-w/2, ScreenHeight/2-h/2)
134+
op.ColorScale.ScaleWithColor(color.White)
135+
text.Draw(
136+
screen,
137+
title,
138+
face,
139+
op,
140+
)
141+
}
142+
143+
// Draw - handle screen update
144+
func (g *Game) Draw(screen *ebiten.Image) {
145+
for _, p := range g.snake {
146+
vector.DrawFilledRect(
147+
screen,
148+
float32(p.x*gridSize),
149+
float32(p.y*gridSize),
150+
gridSize,
151+
gridSize,
152+
color.White,
153+
true,
154+
)
155+
}
156+
vector.DrawFilledRect(
157+
screen,
158+
float32(g.food.x*gridSize),
159+
float32(g.food.y*gridSize),
160+
gridSize,
161+
gridSize,
162+
color.RGBA{255, 0, 0, 255},
163+
true,
164+
)
165+
if g.gameOver {
166+
g.drawGameOverText(screen)
167+
}
168+
}
169+
170+
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
171+
return ScreenWidth, ScreenHeight
172+
}
173+
174+
// SpawnFood - generate new food
175+
func (g *Game) SpawnFood() {
176+
g.food = Point{
177+
x: g.randGenerator.Intn(ScreenWidth / gridSize),
178+
y: g.randGenerator.Intn(ScreenHeight / gridSize),
179+
}
180+
}
181+
182+
// NewGame - create Game
183+
func NewGame() *Game {
184+
return &Game{
185+
snake: []Point{{
186+
x: ScreenWidth / gridSize / 2,
187+
y: ScreenHeight / gridSize / 2,
188+
}},
189+
direction: Point{x: 1, y: 0},
190+
randGenerator: rand.New(rand.NewSource(time.Now().UnixNano())),
191+
}
192+
}
193+
```
194+
195+
## game screen
196+
197+
![snake-game](snake-game.png)

Taskfile.yml

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
version: '3'
2+
3+
dotenv: ['.env']
4+
5+
tasks:
6+
default:
7+
cmds:
8+
- echo "DB_URL=$DB_URL"
9+
silent: true
10+
11+
# build:
12+
# cmds:
13+
# - CGO_ENABLED=0 GOOS=linux go build -o bin/main cmd/main.go
14+
# silent: true
15+
# run:
16+
# cmds:
17+
# - ./bin/main
18+
# deps:
19+
# - build
20+
# silent: true
21+
22+
build-mage:
23+
cmds:
24+
- CGO_ENABLED=0 GOOS=linux go build -o ./mage mage-tools/mage.go
25+
silent: true
26+
27+
build-gg:
28+
cmds:
29+
- ./mage -d mage-tools -compile ../gg
30+
deps:
31+
- build-mage
32+
silent: true
33+
34+
coverage:
35+
cmds:
36+
- go test -v -cover ./...
37+
silent: true
38+
test:
39+
cmds:
40+
- go test -v ./...
41+
silent: true
42+

cmd/game-window-sample/main.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"log"
6+
7+
"github.com/hajimehoshi/ebiten/v2"
8+
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
9+
"github.com/hajimehoshi/ebiten/v2/text/v2"
10+
"github.com/leetcode-golang-classroom/golang-sample-with-snake-game/internal"
11+
)
12+
13+
// MustSetupFonts - setup font source
14+
func MustSetupFonts() {
15+
source, err := text.NewGoTextFaceSource(
16+
bytes.NewReader(
17+
fonts.MPlus1pRegular_ttf,
18+
),
19+
)
20+
if err != nil {
21+
log.Fatal(err)
22+
}
23+
// setup font source
24+
internal.MplusFaceSource = source
25+
}
26+
27+
func main() {
28+
// setup font
29+
MustSetupFonts()
30+
game := internal.NewGame()
31+
game.SpawnFood()
32+
// setup window size
33+
ebiten.SetWindowSize(internal.ScreenWidth, internal.ScreenHeight)
34+
ebiten.SetWindowTitle("Snake")
35+
if err := ebiten.RunGame(game); err != nil {
36+
log.Fatal(err)
37+
}
38+
}

go.mod

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module github.com/leetcode-golang-classroom/golang-sample-with-snake-game
2+
3+
go 1.24.0
4+
5+
require (
6+
github.com/hajimehoshi/ebiten/v2 v2.8.6
7+
github.com/magefile/mage v1.15.0
8+
)
9+
10+
require (
11+
github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect
12+
github.com/ebitengine/hideconsole v1.0.0 // indirect
13+
github.com/ebitengine/purego v0.8.0 // indirect
14+
github.com/go-text/typesetting v0.2.0 // indirect
15+
github.com/jezek/xgb v1.1.1 // indirect
16+
golang.org/x/image v0.20.0 // indirect
17+
golang.org/x/sync v0.8.0 // indirect
18+
golang.org/x/sys v0.25.0 // indirect
19+
golang.org/x/text v0.18.0 // indirect
20+
)

go.sum

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM=
2+
github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY=
3+
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
4+
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
5+
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
6+
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
7+
github.com/go-text/typesetting v0.2.0 h1:fbzsgbmk04KiWtE+c3ZD4W2nmCRzBqrqQOvYlwAOdho=
8+
github.com/go-text/typesetting v0.2.0/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I=
9+
github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY=
10+
github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
11+
github.com/hajimehoshi/bitmapfont/v3 v3.2.0 h1:0DISQM/rseKIJhdF29AkhvdzIULqNIIlXAGWit4ez1Q=
12+
github.com/hajimehoshi/bitmapfont/v3 v3.2.0/go.mod h1:8gLqGatKVu0pwcNCJguW3Igg9WQqVXF0zg/RvrGQWyg=
13+
github.com/hajimehoshi/ebiten/v2 v2.8.6 h1:Dkd/sYI0TYyZRCE7GVxV59XC+WCi2BbGAbIBjXeVC1U=
14+
github.com/hajimehoshi/ebiten/v2 v2.8.6/go.mod h1:cCQ3np7rdmaJa1ZnvslraVlpxNb3wCjEnAP1LHNyXNA=
15+
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
16+
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
17+
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
18+
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
19+
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
20+
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
21+
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
22+
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
23+
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
24+
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
25+
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
26+
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
27+
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
28+
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=

0 commit comments

Comments
 (0)