Skip to content

Commit 7219301

Browse files
committed
add a qix/xonix like game (powered by ebiten and based on my MicroPython version at https://github.com/SimonWaldherr/DIY-Arcade-Machine)
Signed-off-by: Simon Waldherr <git@simon.waldherr.eu>
1 parent 813ab89 commit 7219301

File tree

2 files changed

+365
-0
lines changed

2 files changed

+365
-0
lines changed

non-std-lib/ebiten-qix.go

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"image/color"
6+
"log"
7+
"math/rand"
8+
"time"
9+
10+
"github.com/hajimehoshi/ebiten/v2"
11+
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
12+
)
13+
14+
const (
15+
screenWidth = 320 // Width of the playing field
16+
screenHeight = 240 // Height of the playing field
17+
gridSize = 5 // Size of each grid cell
18+
initialSpeed = 3 // Frames between opponent moves
19+
winPercentage = 75.0 // Percentage to win the game
20+
initialOpponentX = screenWidth / (2 * gridSize)
21+
initialOpponentY = screenHeight / (2 * gridSize)
22+
)
23+
24+
// Grid cell states
25+
const (
26+
Empty = 0 // Empty cell
27+
Border = 1 // Border cell
28+
Captured = 2 // Captured area
29+
Opponent = 3 // Opponent cell
30+
PlayerTrail = 4 // Player trail
31+
AccessibleArea = 5 // Used during flood fill
32+
)
33+
34+
var gameOver bool
35+
36+
// Directions for movement
37+
var (
38+
JOYSTICK_UP = ebiten.KeyArrowUp
39+
JOYSTICK_DOWN = ebiten.KeyArrowDown
40+
JOYSTICK_LEFT = ebiten.KeyArrowLeft
41+
JOYSTICK_RIGHT = ebiten.KeyArrowRight
42+
)
43+
44+
// QixGame structure holds the game state
45+
type QixGame struct {
46+
playerX, playerY int // Player position
47+
opponentX, opponentY int // Opponent position
48+
opponentDX, opponentDY int // Opponent direction
49+
occupiedPercentage float64 // Percentage of the playfield occupied
50+
grid [][]int // Game grid
51+
prevPlayerPos int // Previous player position
52+
width, height int // Grid dimensions
53+
frameCount int // Frame counter
54+
moveInterval int // Frames between opponent moves
55+
}
56+
57+
// Initialize a new game
58+
func NewGame() *QixGame {
59+
game := &QixGame{
60+
playerX: 0,
61+
playerY: 0,
62+
opponentX: initialOpponentX,
63+
opponentY: initialOpponentY,
64+
opponentDX: 1,
65+
opponentDY: 1,
66+
width: screenWidth / gridSize,
67+
height: screenHeight / gridSize,
68+
prevPlayerPos: 1,
69+
occupiedPercentage: 0.0,
70+
moveInterval: initialSpeed,
71+
}
72+
73+
// Initialize the grid with Empty
74+
game.grid = make([][]int, game.width)
75+
for i := range game.grid {
76+
game.grid[i] = make([]int, game.height)
77+
}
78+
79+
game.initializeGame()
80+
return game
81+
}
82+
83+
// Initialize game by drawing borders, placing player and opponent
84+
func (g *QixGame) initializeGame() {
85+
g.drawFrame()
86+
g.placePlayer()
87+
g.placeOpponent()
88+
}
89+
90+
// Draw frame (border)
91+
func (g *QixGame) drawFrame() {
92+
for x := 0; x < g.width; x++ {
93+
g.grid[x][0] = Border
94+
g.grid[x][g.height-1] = Border
95+
}
96+
for y := 0; y < g.height; y++ {
97+
g.grid[0][y] = Border
98+
g.grid[g.width-1][y] = Border
99+
}
100+
}
101+
102+
// Place player at a random position on the edge
103+
func (g *QixGame) placePlayer() {
104+
edgePositions := []struct{ x, y int }{}
105+
for x := 0; x < g.width; x++ {
106+
edgePositions = append(edgePositions, struct{ x, y int }{x, 0})
107+
edgePositions = append(edgePositions, struct{ x, y int }{x, g.height - 1})
108+
}
109+
for y := 1; y < g.height-1; y++ {
110+
edgePositions = append(edgePositions, struct{ x, y int }{0, y})
111+
edgePositions = append(edgePositions, struct{ x, y int }{g.width - 1, y})
112+
}
113+
114+
pos := edgePositions[rand.Intn(len(edgePositions))]
115+
g.playerX, g.playerY = pos.x, pos.y
116+
}
117+
118+
// Place opponent at random position inside the playfield
119+
func (g *QixGame) placeOpponent() {
120+
for {
121+
x := rand.Intn(g.width-2) + 1
122+
y := rand.Intn(g.height-2) + 1
123+
if g.grid[x][y] == Empty {
124+
g.opponentX, g.opponentY = x, y
125+
g.grid[g.opponentX][g.opponentY] = Opponent
126+
break
127+
}
128+
}
129+
}
130+
131+
// Update is called on every frame update
132+
func (g *QixGame) Update() error {
133+
if gameOver {
134+
if ebiten.IsKeyPressed(ebiten.KeySpace) {
135+
*g = *NewGame()
136+
gameOver = false
137+
}
138+
return nil
139+
}
140+
141+
g.frameCount++
142+
143+
// Move opponent every 'moveInterval' frames
144+
if g.frameCount%g.moveInterval == 0 {
145+
g.moveOpponent()
146+
}
147+
148+
// Move player based on input
149+
g.movePlayer()
150+
151+
// Check win condition
152+
if g.occupiedPercentage >= winPercentage {
153+
gameOver = true
154+
}
155+
156+
return nil
157+
}
158+
159+
// Draw the game frame
160+
func (g *QixGame) Draw(screen *ebiten.Image) {
161+
// Clear screen
162+
screen.Fill(color.Black)
163+
164+
// Draw grid and game objects
165+
for x := 0; x < g.width; x++ {
166+
for y := 0; y < g.height; y++ {
167+
switch g.grid[x][y] {
168+
case Border:
169+
ebitenutil.DrawRect(screen, float64(x*gridSize), float64(y*gridSize), gridSize, gridSize, color.RGBA{0, 0, 255, 255})
170+
case Captured:
171+
ebitenutil.DrawRect(screen, float64(x*gridSize), float64(y*gridSize), gridSize, gridSize, color.RGBA{0, 0, 255, 255})
172+
case Opponent:
173+
ebitenutil.DrawRect(screen, float64(x*gridSize), float64(y*gridSize), gridSize, gridSize, color.RGBA{255, 0, 0, 255})
174+
case PlayerTrail:
175+
ebitenutil.DrawRect(screen, float64(x*gridSize), float64(y*gridSize), gridSize, gridSize, color.RGBA{0, 255, 0, 255})
176+
}
177+
}
178+
}
179+
180+
// Draw player
181+
ebitenutil.DrawRect(screen, float64(g.playerX*gridSize), float64(g.playerY*gridSize), gridSize, gridSize, color.RGBA{0, 255, 0, 255})
182+
183+
// Display score (occupied percentage)
184+
scoreMsg := fmt.Sprintf("Occupied: %.2f%%", g.occupiedPercentage)
185+
ebitenutil.DebugPrintAt(screen, scoreMsg, 10, screenHeight-20)
186+
187+
// Display win message
188+
if g.occupiedPercentage >= winPercentage {
189+
msg := "YOU WIN! Press 'Space' to Restart"
190+
ebitenutil.DebugPrintAt(screen, msg, screenWidth/2-100, screenHeight/2-10)
191+
}
192+
193+
// Display game over message
194+
if gameOver && g.occupiedPercentage < winPercentage {
195+
msg := "GAME OVER! Press 'Space' to Restart"
196+
ebitenutil.DebugPrintAt(screen, msg, screenWidth/2-100, screenHeight/2-10)
197+
}
198+
}
199+
200+
// Layout defines the game window size
201+
func (g *QixGame) Layout(outsideWidth, outsideHeight int) (int, int) {
202+
return screenWidth, screenHeight
203+
}
204+
205+
// Move the player based on keyboard input
206+
func (g *QixGame) movePlayer() {
207+
var newX, newY int = g.playerX, g.playerY
208+
if ebiten.IsKeyPressed(JOYSTICK_UP) {
209+
newY--
210+
} else if ebiten.IsKeyPressed(JOYSTICK_DOWN) {
211+
newY++
212+
} else if ebiten.IsKeyPressed(JOYSTICK_LEFT) {
213+
newX--
214+
} else if ebiten.IsKeyPressed(JOYSTICK_RIGHT) {
215+
newX++
216+
} else {
217+
return // No movement key pressed
218+
}
219+
220+
// Ensure player stays within the bounds
221+
if newX >= 0 && newX < g.width && newY >= 0 && newY < g.height {
222+
cellValue := g.grid[newX][newY]
223+
switch cellValue {
224+
case Empty:
225+
// Moving into empty space, mark trail
226+
g.grid[newX][newY] = PlayerTrail
227+
g.prevPlayerPos = 0
228+
case Border, Captured:
229+
if g.prevPlayerPos == 0 {
230+
g.closeArea(newX, newY)
231+
}
232+
g.prevPlayerPos = 1
233+
case Opponent, PlayerTrail:
234+
// Collision, game over
235+
gameOver = true
236+
return
237+
}
238+
g.playerX, g.playerY = newX, newY
239+
}
240+
}
241+
242+
// Close the area when player reconnects to the border or their trail
243+
func (g *QixGame) closeArea(x, y int) {
244+
// Convert all player trails to permanent borders
245+
for i := 0; i < g.width; i++ {
246+
for j := 0; j < g.height; j++ {
247+
if g.grid[i][j] == PlayerTrail {
248+
g.grid[i][j] = Border
249+
}
250+
}
251+
}
252+
253+
// Perform flood fill from the opponent's position
254+
g.floodFill(g.opponentX, g.opponentY)
255+
256+
// Mark areas not accessible from the opponent as captured
257+
for i := 0; i < g.width; i++ {
258+
for j := 0; j < g.height; j++ {
259+
switch g.grid[i][j] {
260+
case Empty:
261+
g.grid[i][j] = Captured
262+
case AccessibleArea:
263+
g.grid[i][j] = Empty
264+
}
265+
}
266+
}
267+
268+
// Re-mark the opponent's position
269+
g.grid[g.opponentX][g.opponentY] = Opponent
270+
271+
// Recalculate occupied percentage
272+
g.calculateOccupiedPercentage()
273+
}
274+
275+
// Flood fill to determine the opponent's accessible area
276+
func (g *QixGame) floodFill(x, y int) {
277+
queue := []struct{ x, y int }{{x, y}}
278+
visited := make(map[[2]int]bool)
279+
visited[[2]int{x, y}] = true
280+
281+
for len(queue) > 0 {
282+
current := queue[0]
283+
queue = queue[1:]
284+
285+
directions := []struct{ dx, dy int }{
286+
{1, 0}, {-1, 0}, {0, 1}, {0, -1},
287+
}
288+
289+
for _, dir := range directions {
290+
nx, ny := current.x+dir.dx, current.y+dir.dy
291+
if nx >= 0 && nx < g.width && ny >= 0 && ny < g.height {
292+
if !visited[[2]int{nx, ny}] && (g.grid[nx][ny] == Empty || g.grid[nx][ny] == Opponent) {
293+
g.grid[nx][ny] = AccessibleArea
294+
visited[[2]int{nx, ny}] = true
295+
queue = append(queue, struct{ x, y int }{nx, ny})
296+
}
297+
}
298+
}
299+
}
300+
}
301+
302+
// Calculate how much of the playfield has been occupied
303+
func (g *QixGame) calculateOccupiedPercentage() {
304+
total := g.width * g.height
305+
occupied := 0
306+
for x := 0; x < g.width; x++ {
307+
for y := 0; y < g.height; y++ {
308+
if g.grid[x][y] == Captured {
309+
occupied++
310+
}
311+
}
312+
}
313+
g.occupiedPercentage = (float64(occupied) / float64(total)) * 100
314+
}
315+
316+
// Move the opponent and handle collisions
317+
func (g *QixGame) moveOpponent() {
318+
nextX := g.opponentX + g.opponentDX
319+
nextY := g.opponentY + g.opponentDY
320+
321+
// Check for collision with borders or captured areas
322+
if g.grid[nextX][nextY] == Border || g.grid[nextX][nextY] == Captured {
323+
// Reverse direction upon collision
324+
if g.grid[nextX][g.opponentY] == Border || g.grid[nextX][g.opponentY] == Captured {
325+
g.opponentDX = -g.opponentDX
326+
}
327+
if g.grid[g.opponentX][nextY] == Border || g.grid[g.opponentX][nextY] == Captured {
328+
g.opponentDY = -g.opponentDY
329+
}
330+
nextX = g.opponentX + g.opponentDX
331+
nextY = g.opponentY + g.opponentDY
332+
}
333+
334+
// Check for collision with player trail or player
335+
if g.grid[nextX][nextY] == PlayerTrail || (nextX == g.playerX && nextY == g.playerY) {
336+
gameOver = true
337+
return
338+
}
339+
340+
// Clear current position
341+
g.grid[g.opponentX][g.opponentY] = Empty
342+
343+
// Update position
344+
g.opponentX = nextX
345+
g.opponentY = nextY
346+
347+
// Set new position
348+
g.grid[g.opponentX][g.opponentY] = Opponent
349+
}
350+
351+
// Main loop
352+
func (g *QixGame) Run() {
353+
ebiten.SetWindowSize(screenWidth*2, screenHeight*2)
354+
ebiten.SetWindowTitle("Qix/Xonix Game")
355+
356+
if err := ebiten.RunGame(g); err != nil {
357+
log.Fatal(err)
358+
}
359+
}
360+
361+
func main() {
362+
rand.Seed(time.Now().UnixNano())
363+
game := NewGame()
364+
game.Run()
365+
}
File renamed without changes.

0 commit comments

Comments
 (0)