You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
409 lines
7.7 KiB
409 lines
7.7 KiB
2 years ago
|
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 "<unknown>"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 "<unknown>"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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.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)
|
||
|
}
|
||
|
}
|