Skip to content

leetcode-golang-classroom/golang-sample-with-snake-game

Repository files navigation

golang-sample-with-snake-game

This repository is learn how to use ebiten to build a snake game

game logic

package internal

import (
	"image/color"
	"math/rand"
	"time"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/text/v2"
	"github.com/hajimehoshi/ebiten/v2/vector"
)

var (
	dirUp           = Point{x: 0, y: -1}
	dirDown         = Point{x: 0, y: 1}
	dirLeft         = Point{x: -1, y: 0}
	dirRight        = Point{x: 1, y: 0}
	MplusFaceSource *text.GoTextFaceSource
)

const (
	gameSpeed    = time.Second / 6
	ScreenWidth  = 640
	ScreenHeight = 480
	gridSize     = 20
)

type Point struct {
	x, y int
}

type Game struct {
	snake         []Point
	direction     Point
	lastUpdate    time.Time
	food          Point
	randGenerator *rand.Rand
	gameOver      bool
}

func (g *Game) Update() error {
	if g.gameOver {
		return nil
	}
	// handle key
	if ebiten.IsKeyPressed(ebiten.KeyW) {
		g.direction = dirUp
	} else if ebiten.IsKeyPressed(ebiten.KeyS) {
		g.direction = dirDown
	} else if ebiten.IsKeyPressed(ebiten.KeyA) {
		g.direction = dirLeft
	} else if ebiten.IsKeyPressed(ebiten.KeyD) {
		g.direction = dirRight
	}
	// slow down
	if time.Since(g.lastUpdate) < gameSpeed {
		return nil
	}
	g.lastUpdate = time.Now()

	g.updateSnake(&g.snake, g.direction)
	return nil
}

// isBadCollision - check if snake is collision
func (g Game) isBadCollision(
	newHead Point,
	snake []Point,
) bool {
	// check if out of bound
	if newHead.x < 0 || newHead.y < 0 ||
		newHead.x >= ScreenWidth/gridSize || newHead.y >= ScreenHeight/gridSize {
		return true
	}
	// is newhead collision
	for _, snakeBody := range snake {
		if snakeBody == newHead {
			return true
		}
	}
	return false
}

// updateSnake - update snake with direction
func (g *Game) updateSnake(snake *[]Point, direction Point) {
	head := (*snake)[0]

	newHead := Point{
		x: head.x + direction.x,
		y: head.y + direction.y,
	}

	// check collision for snake
	if g.isBadCollision(newHead, *snake) {
		g.gameOver = true
		return
	}
	// check collision with food
	if newHead == g.food {
		*snake = append(
			[]Point{newHead},
			*snake...,
		)
		g.SpawnFood()
	} else {
		*snake = append(
			[]Point{newHead},
			(*snake)[:len(*snake)-1]...,
		)
	}

}

// drawGameOverText - draw game over text on screen
func (g *Game) drawGameOverText(screen *ebiten.Image) {
	face := &text.GoTextFace{
		Source: MplusFaceSource,
		Size:   48,
	}
	title := "Game Over!"
	w, h := text.Measure(title,
		face,
		face.Size,
	)
	op := &text.DrawOptions{}
	op.GeoM.Translate(ScreenWidth/2-w/2, ScreenHeight/2-h/2)
	op.ColorScale.ScaleWithColor(color.White)
	text.Draw(
		screen,
		title,
		face,
		op,
	)
}

// Draw - handle screen update
func (g *Game) Draw(screen *ebiten.Image) {
	for _, p := range g.snake {
		vector.DrawFilledRect(
			screen,
			float32(p.x*gridSize),
			float32(p.y*gridSize),
			gridSize,
			gridSize,
			color.White,
			true,
		)
	}
	vector.DrawFilledRect(
		screen,
		float32(g.food.x*gridSize),
		float32(g.food.y*gridSize),
		gridSize,
		gridSize,
		color.RGBA{255, 0, 0, 255},
		true,
	)
	if g.gameOver {
		g.drawGameOverText(screen)
	}
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
	return ScreenWidth, ScreenHeight
}

// SpawnFood - generate new food
func (g *Game) SpawnFood() {
	g.food = Point{
		x: g.randGenerator.Intn(ScreenWidth / gridSize),
		y: g.randGenerator.Intn(ScreenHeight / gridSize),
	}
}

// NewGame - create Game
func NewGame() *Game {
	return &Game{
		snake: []Point{{
			x: ScreenWidth / gridSize / 2,
			y: ScreenHeight / gridSize / 2,
		}},
		direction:     Point{x: 1, y: 0},
		randGenerator: rand.New(rand.NewSource(time.Now().UnixNano())),
	}
}

game screen

snake-game

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages