package main import ( "fmt" "image/color" "log" "math/rand" "time" "golang.org/x/image/font" "golang.org/x/image/font/opentype" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text" ) var ( mplusNormalFont font.Face ) func init() { // TODO: don't use the ebiten example code font tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf) if err != nil { log.Fatal(err) } const dpi = 72 mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ Size: 24, DPI: dpi, Hinting: font.HintingFull, }) if err != nil { log.Fatal(err) } rand.Seed(time.Now().UnixMicro()) } type Kind uint8 const ( Empty Kind = iota Worm Food ) func (k Kind) String() string { switch k { case Empty: return "Empty" case Worm: return "Worm" case Food: return "Food" default: return "" } } type Direction uint8 const ( None Direction = iota North South East West ) func (d Direction) String() string { switch d { case None: return "None" case North: return "North" case South: return "South" case East: return "East" case West: return "West" default: return "" } } type Tile struct { kind Kind dir Direction } func (t Tile) String() string { return fmt.Sprintf("%s -- %s", t.kind, t.dir) } var EmptyTile = Tile{kind: Empty, dir: None} var FoodTile = Tile{kind: Food, dir: None} type point struct { x, y int } type WormGame struct { screenWidth, screenHeight int width, height int initLength int score int won, lost bool board [][]Tile head point tail point lastMove time.Time speed time.Duration } func NewGame(screenWidth, screenHeight int, initLength int, speed time.Duration) *WormGame { w := &WormGame{ screenWidth: screenWidth, screenHeight: screenHeight, width: screenWidth / 4, height: screenHeight / 4, initLength: initLength, speed: speed, } w.Reset() return w } func (w *WormGame) Reset() { w.lost = false w.won = false w.score = 0 w.board = make([][]Tile, w.width) for idx := range w.board { w.board[idx] = make([]Tile, w.height) } w.head = point{ x: w.width / 2, y: w.height / 2, } w.Set(w.head, Worm, East) for idx := 1; idx < w.initLength; idx++ { w.board[w.head.x-idx][w.head.y] = Tile{kind: Worm, dir: East} } w.tail = point{w.head.x - w.initLength + 1, w.head.y} w.RandomizeFood() w.lastMove = time.Now() } func (w *WormGame) randomFreeSpace() *point { checked := map[point]struct{}{} for len(checked) <= w.width*w.height { x := rand.Intn(w.width) y := rand.Intn(w.height) p := point{x, y} if _, ok := checked[p]; ok { continue } checked[p] = struct{}{} if w.board[x][y].kind == Empty { return &p } } return nil } func (w *WormGame) RandomizeFood() { p := w.randomFreeSpace() if p == nil { w.won = true return } w.board[p.x][p.y] = FoodTile } func (w *WormGame) InBounds(p point) bool { return p.x < w.width && p.x >= 0 && p.y < w.height && p.y >= 0 } func (w *WormGame) MustInBounds(p point) { if !w.InBounds(p) { panic("out of bounds!") } } func (w *WormGame) Get(p point) Tile { w.MustInBounds(p) return w.board[p.x][p.y] } func (w *WormGame) Set(p point, kind Kind, dir Direction) { w.MustInBounds(p) w.board[p.x][p.y] = Tile{kind: kind, dir: dir} } func (w *WormGame) SetEmpty(p point) { w.MustInBounds(p) w.board[p.x][p.y] = EmptyTile } func (w *WormGame) AdvanceWorm() { dir := w.Get(w.head).dir prev := w.head switch dir { case North: if w.head.y-1 > w.height { panic("out of bounds") } w.head.y -= 1 case South: if w.head.y+1 < 0 { panic("out of bounds") } w.head.y += 1 case East: if w.head.x+1 > w.width { panic("out of bounds") } w.head.x += 1 case West: if w.head.x-1 < 0 { panic("out of bounds") } w.head.x -= 1 } // change direction of prev worm segment w.Set(prev, Worm, dir) // add new worm segment w.Set(w.head, Worm, dir) tailDir := w.Get(w.tail).dir // shorten tail w.SetEmpty(w.tail) // update tail pointer switch tailDir { case North: w.tail = point{w.tail.x, w.tail.y - 1} case South: w.tail = point{w.tail.x, w.tail.y + 1} case East: w.tail = point{w.tail.x + 1, w.tail.y} case West: w.tail = point{w.tail.x - 1, w.tail.y} } // update laste move ts w.lastMove = time.Now() } func (w *WormGame) EmbiggenWorm() { tailDir := w.Get(w.tail).dir // update tail pointer switch tailDir { case North: w.tail = point{w.tail.x, w.tail.y + 1} case South: w.tail = point{w.tail.x, w.tail.y - 1} case East: w.tail = point{w.tail.x - 1, w.tail.y} case West: w.tail = point{w.tail.x + 1, w.tail.y} } w.Set(w.tail, Worm, tailDir) } func (w *WormGame) Update() error { if w.won || w.lost { if inpututil.IsKeyJustPressed(ebiten.KeySpace) { w.Reset() } return nil } up := inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) down := inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) left := inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) right := inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) // cancel out simultaneous up and down if up && down { up = false down = false } // cancel up simultaneous left and right, // also up/down take priority over left/right if (left && right) || (up || down) { left = false right = false } changedDirection := true headTile := w.Get(w.head) if up && headTile.dir != South { headTile.dir = North } else if down && headTile.dir != North { headTile.dir = South } else if left && headTile.dir != East { headTile.dir = West } else if right && headTile.dir != West { headTile.dir = East } else { changedDirection = false } w.Set(w.head, headTile.kind, headTile.dir) var next point switch headTile.dir { case North: next = point{w.head.x, w.head.y - 1} case South: next = point{w.head.x, w.head.y + 1} case East: next = point{w.head.x + 1, w.head.y} case West: next = point{w.head.x - 1, w.head.y} } // don't evaluate space or advance if we didn't change direction and haven't reached timeout if !changedDirection && time.Now().Sub(w.lastMove) < w.speed { return nil } var ate bool if next.x >= w.width || next.y >= w.height || next.x < 0 || next.y < 0 { w.lost = true } else { tile := w.board[next.x][next.y] ate = (tile.kind == Food) w.lost = (tile.kind == Worm) } if w.lost { return nil } if ate { w.score += 1 w.EmbiggenWorm() w.RandomizeFood() } // check if we won (all spaces are worm) and skip moving (since it would be // an auto-loss) this is probably close to impossible? if w.won { return nil } w.AdvanceWorm() return nil } func (w *WormGame) Draw(screen *ebiten.Image) { screen.Clear() for col := range w.board { for row := range w.board[col] { x := float64(col * 4) y := float64(row * 4) switch w.board[col][row].kind { case Empty: ebitenutil.DrawRect(screen, x, y, 4, 4, color.Black) case Worm: ebitenutil.DrawRect(screen, x, y, 4, 4, color.White) case Food: ebitenutil.DrawRect(screen, x, y, 4, 4, color.White) } } } if w.won { text.Draw(screen, fmt.Sprintf("You Win! Score: %d", w.score), mplusNormalFont, 20, w.screenHeight/2, color.White) return } else if w.lost { text.Draw(screen, fmt.Sprintf("Game Over. Score: %d", w.score), mplusNormalFont, 20, w.screenHeight/2, color.White) return } } func (w *WormGame) Layout(outsideWidth, outsideHeight int) (int, int) { return w.screenWidth, w.screenHeight } const ( screenWidth = 320 screenHeight = 240 ) func main() { game := NewGame(screenWidth, screenHeight, 4, 75*time.Millisecond) ebiten.SetWindowSize(screenWidth*2, screenHeight*2) ebiten.SetWindowTitle("Worm") if err := ebiten.RunGame(game); err != nil { log.Fatal(err) } }