v0
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
package colors
|
||||||
|
|
||||||
|
import "image/color"
|
||||||
|
|
||||||
|
var (
|
||||||
|
Black = color.RGBA{0, 0, 0, 1}
|
||||||
|
LightGray = color.RGBA{24, 24, 24, 1}
|
||||||
|
Red = color.RGBA{186, 46, 46, 1}
|
||||||
|
DarkYellow = color.RGBA{184, 178, 94, 1}
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package buttons
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/UnintendedFraud/snake-game/colors"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MenuAction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StartGame MenuAction = iota
|
||||||
|
ExitGame
|
||||||
|
ReturnToMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
type MenuButton struct {
|
||||||
|
Value string
|
||||||
|
Action MenuAction
|
||||||
|
Img *ebiten.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MenuButton) Execute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MenuButton) GetMenuButtonImg(
|
||||||
|
font *text.GoTextFaceSource,
|
||||||
|
fontSize float64,
|
||||||
|
isFocused bool,
|
||||||
|
) *ebiten.Image {
|
||||||
|
img := ebiten.NewImage(200, 50)
|
||||||
|
|
||||||
|
fontOptions := &text.DrawOptions{}
|
||||||
|
if isFocused {
|
||||||
|
fontOptions.ColorScale.ScaleWithColor(colors.Red)
|
||||||
|
} else {
|
||||||
|
fontOptions.ColorScale.ScaleWithColor(color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
text.Draw(
|
||||||
|
img,
|
||||||
|
b.Value,
|
||||||
|
&text.GoTextFace{
|
||||||
|
Source: font,
|
||||||
|
Size: fontSize,
|
||||||
|
},
|
||||||
|
fontOptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MenuButton) Clear() {
|
||||||
|
b.Img.Clear()
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WINDOW_WIDTH int = 1200
|
||||||
|
WINDOW_HEIGHT int = 800
|
||||||
|
)
|
||||||
|
|
||||||
|
type GameState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MainMenu GameState = iota
|
||||||
|
Playing
|
||||||
|
Dead
|
||||||
|
)
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
font *text.GoTextFaceSource
|
||||||
|
|
||||||
|
titleImg *ebiten.Image
|
||||||
|
|
||||||
|
state GameState
|
||||||
|
snake *Snake
|
||||||
|
menu *Menu
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitGame() *Game {
|
||||||
|
ebiten.SetWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||||
|
ebiten.SetWindowTitle("Snake")
|
||||||
|
|
||||||
|
fontFile, err := os.Open("fonts/rockers_garage.ttf")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
font, err := text.NewGoTextFaceSource(fontFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Game{
|
||||||
|
font: font,
|
||||||
|
titleImg: initTitle(font),
|
||||||
|
state: MainMenu,
|
||||||
|
snake: InitSnake(WINDOW_WIDTH, WINDOW_HEIGHT),
|
||||||
|
menu: InitMenu(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Update() error {
|
||||||
|
switch g.state {
|
||||||
|
case MainMenu:
|
||||||
|
g.menu.UpdateFocus()
|
||||||
|
g.menu.Click(g)
|
||||||
|
|
||||||
|
case Playing:
|
||||||
|
g.snake.Eat()
|
||||||
|
g.snake.Sprint()
|
||||||
|
g.snake.ManageDirection()
|
||||||
|
g.snake.Move()
|
||||||
|
|
||||||
|
if g.snake.HasCollided() {
|
||||||
|
g.snake.isDead = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.snake.isDead {
|
||||||
|
g.snake.Click(g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
|
screen.DrawImage(g.titleImg, &ebiten.DrawImageOptions{})
|
||||||
|
|
||||||
|
switch g.state {
|
||||||
|
case MainMenu:
|
||||||
|
g.menu.Render(screen, g.font)
|
||||||
|
|
||||||
|
case Playing:
|
||||||
|
g.snake.Render(screen, g.font)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Layout(outsideWith, outsideHeight int) (screenWidth, screenHeight int) {
|
||||||
|
return WINDOW_WIDTH, WINDOW_HEIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
func initTitle(font *text.GoTextFaceSource) *ebiten.Image {
|
||||||
|
face := &text.GoTextFace{
|
||||||
|
Source: font,
|
||||||
|
Size: 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
titleW, _ := text.Measure("snake", face, 0)
|
||||||
|
|
||||||
|
fontOptions := &text.DrawOptions{}
|
||||||
|
fontOptions.GeoM.Translate(float64(WINDOW_WIDTH)/2-titleW/2, 10)
|
||||||
|
fontOptions.ColorScale.ScaleWithColor(color.RGBA{194, 169, 60, 1})
|
||||||
|
|
||||||
|
h := float64(WINDOW_HEIGHT) * 0.1
|
||||||
|
|
||||||
|
titleImg := ebiten.NewImage(WINDOW_WIDTH, int(h))
|
||||||
|
|
||||||
|
text.Draw(titleImg, "snake", face, fontOptions)
|
||||||
|
|
||||||
|
return titleImg
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/UnintendedFraud/snake-game/components/buttons"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Menu struct {
|
||||||
|
focusedIdx int
|
||||||
|
buttons []buttons.MenuButton
|
||||||
|
img *ebiten.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitMenu() *Menu {
|
||||||
|
return &Menu{
|
||||||
|
img: ebiten.NewImage(200, 200),
|
||||||
|
focusedIdx: 0,
|
||||||
|
buttons: []buttons.MenuButton{
|
||||||
|
{
|
||||||
|
Value: "start game",
|
||||||
|
Action: buttons.StartGame,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: "exit game",
|
||||||
|
Action: buttons.ExitGame,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) Click(game *Game) {
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) ||
|
||||||
|
inpututil.IsKeyJustPressed(ebiten.KeyKPEnter) {
|
||||||
|
m.executeAction(game, m.buttons[m.focusedIdx].Action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) executeAction(game *Game, action buttons.MenuAction) {
|
||||||
|
switch action {
|
||||||
|
case buttons.StartGame:
|
||||||
|
game.snake = InitSnake(WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||||
|
game.state = Playing
|
||||||
|
|
||||||
|
case buttons.ExitGame:
|
||||||
|
fmt.Println("Closing the game")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) UpdateFocus() {
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyUp) {
|
||||||
|
if m.focusedIdx != 0 {
|
||||||
|
m.focusedIdx--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyDown) {
|
||||||
|
if m.focusedIdx != len(m.buttons)-1 {
|
||||||
|
m.focusedIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) Render(screen *ebiten.Image, font *text.GoTextFaceSource) {
|
||||||
|
m.Clear()
|
||||||
|
|
||||||
|
for idx, b := range m.buttons {
|
||||||
|
menuImg := b.GetMenuButtonImg(font, 24, m.focusedIdx == idx)
|
||||||
|
|
||||||
|
x := float64(0)
|
||||||
|
y := float64(idx*50 + 50)
|
||||||
|
menuImgOp := &ebiten.DrawImageOptions{}
|
||||||
|
menuImgOp.GeoM.Translate(x, y)
|
||||||
|
m.img.DrawImage(menuImg, menuImgOp)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgOp := &ebiten.DrawImageOptions{}
|
||||||
|
imgOp.GeoM.Translate(150, 200)
|
||||||
|
screen.DrawImage(m.img, imgOp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) Clear() {
|
||||||
|
m.img.Clear()
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math/rand"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/UnintendedFraud/snake-game/colors"
|
||||||
|
"github.com/UnintendedFraud/snake-game/components/buttons"
|
||||||
|
"github.com/UnintendedFraud/snake-game/utils"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Direction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Up Direction = iota
|
||||||
|
Right
|
||||||
|
Down
|
||||||
|
Left
|
||||||
|
)
|
||||||
|
|
||||||
|
type Snake struct {
|
||||||
|
img *ebiten.Image
|
||||||
|
speed int64
|
||||||
|
direction Direction
|
||||||
|
positions []image.Point
|
||||||
|
sprint bool
|
||||||
|
|
||||||
|
ennemies []image.Point
|
||||||
|
|
||||||
|
prevTime time.Time
|
||||||
|
currTime time.Time
|
||||||
|
diff time.Duration
|
||||||
|
|
||||||
|
isDead bool
|
||||||
|
|
||||||
|
total int
|
||||||
|
score int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
START_LENGTH = 20
|
||||||
|
THICKNESS = 8
|
||||||
|
MIN_SPEED int64 = 500
|
||||||
|
MAX_SPEED int64 = 50
|
||||||
|
MAX_SPRINT int64 = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitSnake(width, height int) *Snake {
|
||||||
|
h := int(float64(height) * 0.9)
|
||||||
|
|
||||||
|
img := ebiten.NewImage(width, h)
|
||||||
|
|
||||||
|
return &Snake{
|
||||||
|
img: img,
|
||||||
|
speed: MIN_SPEED,
|
||||||
|
direction: Right,
|
||||||
|
positions: getInitPositions(utils.GetCenter(width, height)),
|
||||||
|
ennemies: randomEnnemies(width, h, 10),
|
||||||
|
isDead: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snake *Snake) Render(screen *ebiten.Image, font *text.GoTextFaceSource) {
|
||||||
|
if snake == nil || snake.img == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snake.img.Clear()
|
||||||
|
|
||||||
|
renderScoreScreen(screen, snake, font)
|
||||||
|
|
||||||
|
if snake.isDead {
|
||||||
|
renderDeadScreen(screen, snake, font)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snake.img.Fill(colors.LightGray)
|
||||||
|
|
||||||
|
for _, ennemy := range snake.ennemies {
|
||||||
|
vector.DrawFilledRect(
|
||||||
|
snake.img,
|
||||||
|
float32(ennemy.X),
|
||||||
|
float32(ennemy.Y),
|
||||||
|
THICKNESS,
|
||||||
|
THICKNESS,
|
||||||
|
colors.DarkYellow,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, point := range snake.positions {
|
||||||
|
vector.DrawFilledRect(
|
||||||
|
snake.img,
|
||||||
|
float32(point.X),
|
||||||
|
float32(point.Y),
|
||||||
|
THICKNESS,
|
||||||
|
THICKNESS,
|
||||||
|
color.White,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &ebiten.DrawImageOptions{}
|
||||||
|
|
||||||
|
ty := float64(screen.Bounds().Dy()) * 0.1
|
||||||
|
options.GeoM.Translate(0, ty)
|
||||||
|
|
||||||
|
screen.DrawImage(snake.img, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snake *Snake) Sprint() {
|
||||||
|
if utils.SliceContains(inpututil.AppendPressedKeys([]ebiten.Key{}), ebiten.KeySpace) && !snake.sprint {
|
||||||
|
snake.sprint = true
|
||||||
|
} else if snake.sprint {
|
||||||
|
snake.sprint = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snake *Snake) ManageDirection() {
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyUp) && snake.direction != Down {
|
||||||
|
snake.direction = Up
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyRight) && snake.direction != Left {
|
||||||
|
snake.direction = Right
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyDown) && snake.direction != Up {
|
||||||
|
snake.direction = Down
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyLeft) && snake.direction != Right {
|
||||||
|
snake.direction = Left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snake *Snake) HasCollided() bool {
|
||||||
|
head := snake.positions[0]
|
||||||
|
maxX := snake.img.Bounds().Dx()
|
||||||
|
maxY := snake.img.Bounds().Dy()
|
||||||
|
|
||||||
|
if head.X <= 0 || head.X >= maxX || head.Y <= 0 || head.Y >= maxY {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return snake.hitItself()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snake *Snake) Eat() {
|
||||||
|
head := snake.positions[0]
|
||||||
|
|
||||||
|
idx, err := utils.SliceIndexOf(snake.ennemies, func(e image.Point) bool {
|
||||||
|
return e.X == head.X && e.Y == head.Y
|
||||||
|
})
|
||||||
|
if err != nil || idx < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snake.ennemies = slices.Delete(snake.ennemies, idx, idx+1)
|
||||||
|
|
||||||
|
snake.positions = append(
|
||||||
|
[]image.Point{snake.getNextPosition()},
|
||||||
|
snake.positions...,
|
||||||
|
)
|
||||||
|
|
||||||
|
if snake.speed > MAX_SPEED {
|
||||||
|
snake.speed -= 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snake *Snake) Move() {
|
||||||
|
snake.prevTime = snake.currTime
|
||||||
|
snake.currTime = time.Now()
|
||||||
|
snake.diff += snake.currTime.Sub(snake.prevTime)
|
||||||
|
|
||||||
|
var speed int64
|
||||||
|
if snake.sprint {
|
||||||
|
speed = MAX_SPRINT
|
||||||
|
} else {
|
||||||
|
speed = snake.speed
|
||||||
|
}
|
||||||
|
|
||||||
|
if snake.diff.Milliseconds() < speed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snake.diff = 0
|
||||||
|
|
||||||
|
snake.positions = append(
|
||||||
|
[]image.Point{snake.getNextPosition()},
|
||||||
|
snake.positions[0:len(snake.positions)-1]...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snake *Snake) hitItself() bool {
|
||||||
|
head := snake.positions[0]
|
||||||
|
|
||||||
|
for i := 1; i < len(snake.positions); i++ {
|
||||||
|
p := snake.positions[i]
|
||||||
|
if head.X == p.X && head.Y == p.Y {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snake *Snake) getNextPosition() image.Point {
|
||||||
|
nextHead := snake.positions[0]
|
||||||
|
|
||||||
|
switch snake.direction {
|
||||||
|
case Up:
|
||||||
|
nextHead.Y = nextHead.Y - THICKNESS
|
||||||
|
case Right:
|
||||||
|
nextHead.X = nextHead.X + THICKNESS
|
||||||
|
case Down:
|
||||||
|
nextHead.Y = nextHead.Y + THICKNESS
|
||||||
|
case Left:
|
||||||
|
nextHead.X = nextHead.X - THICKNESS
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextHead
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snake *Snake) Click(game *Game) {
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) ||
|
||||||
|
inpututil.IsKeyJustPressed(ebiten.KeyKPEnter) {
|
||||||
|
game.state = MainMenu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInitPositions(center image.Point) []image.Point {
|
||||||
|
points := []image.Point{}
|
||||||
|
|
||||||
|
for i := range START_LENGTH {
|
||||||
|
points = append(points, image.Point{X: center.X - i*THICKNESS, Y: center.Y})
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomEnnemies(width int, height int, count int) []image.Point {
|
||||||
|
ennemies := []image.Point{}
|
||||||
|
|
||||||
|
w := width / THICKNESS
|
||||||
|
h := height / THICKNESS
|
||||||
|
|
||||||
|
for range count {
|
||||||
|
x := rand.Intn(w) * THICKNESS
|
||||||
|
y := rand.Intn(h) * THICKNESS
|
||||||
|
|
||||||
|
ennemies = append(ennemies, image.Point{X: x, Y: y})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ennemies
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderScoreScreen(screen *ebiten.Image, snake *Snake, font *text.GoTextFaceSource) {
|
||||||
|
scoreImg := ebiten.NewImage(WINDOW_WIDTH, 100)
|
||||||
|
scoreImg.Fill(colors.Red)
|
||||||
|
|
||||||
|
levelOptions := &text.DrawOptions{}
|
||||||
|
levelOptions.ColorScale.ScaleWithColor(color.White)
|
||||||
|
levelOptions.GeoM.Translate(10, 10)
|
||||||
|
text.Draw(
|
||||||
|
scoreImg,
|
||||||
|
"Level 1",
|
||||||
|
&text.GoTextFace{
|
||||||
|
Source: font,
|
||||||
|
Size: 25,
|
||||||
|
},
|
||||||
|
levelOptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
scoreOptions := &text.DrawOptions{}
|
||||||
|
scoreOptions.ColorScale.ScaleWithColor(color.White)
|
||||||
|
scoreOptions.GeoM.Translate(100, 10)
|
||||||
|
text.Draw(
|
||||||
|
scoreImg,
|
||||||
|
fmt.Sprintf("Score: %d/%d", snake.score, snake.total),
|
||||||
|
&text.GoTextFace{
|
||||||
|
Source: font,
|
||||||
|
Size: 25,
|
||||||
|
},
|
||||||
|
scoreOptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
snake.img.DrawImage(scoreImg, &ebiten.DrawImageOptions{})
|
||||||
|
|
||||||
|
screen.DrawImage(snake.img, &ebiten.DrawImageOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderDeadScreen(screen *ebiten.Image, snake *Snake, font *text.GoTextFaceSource) {
|
||||||
|
snake.img.Fill(colors.Black)
|
||||||
|
|
||||||
|
fontOptions := &text.DrawOptions{}
|
||||||
|
fontOptions.ColorScale.ScaleWithColor(colors.Red)
|
||||||
|
|
||||||
|
deadImg := ebiten.NewImage(450, 90)
|
||||||
|
|
||||||
|
text.Draw(
|
||||||
|
deadImg,
|
||||||
|
"You are DEAD !",
|
||||||
|
&text.GoTextFace{
|
||||||
|
Source: font,
|
||||||
|
Size: 80,
|
||||||
|
},
|
||||||
|
fontOptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
deadImgW := deadImg.Bounds().Dx()
|
||||||
|
deadImgH := deadImg.Bounds().Dy()
|
||||||
|
x := float64(screen.Bounds().Dx()/2 - deadImgW/2)
|
||||||
|
y := float64(screen.Bounds().Dy()/2 - deadImgH/2)
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(x, y)
|
||||||
|
snake.img.DrawImage(deadImg, op)
|
||||||
|
|
||||||
|
// ---- return to menu button
|
||||||
|
|
||||||
|
returnToMenuButton := buttons.MenuButton{
|
||||||
|
Value: "return to menu",
|
||||||
|
Action: buttons.ReturnToMenu,
|
||||||
|
}
|
||||||
|
|
||||||
|
rtmbImg := returnToMenuButton.GetMenuButtonImg(font, 24, true)
|
||||||
|
|
||||||
|
rtmOp := &ebiten.DrawImageOptions{}
|
||||||
|
rtmOp.GeoM.Translate(x, y+100)
|
||||||
|
snake.img.DrawImage(rtmbImg, rtmOp)
|
||||||
|
|
||||||
|
screen.DrawImage(snake.img, &ebiten.DrawImageOptions{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package texts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Text struct {
|
||||||
|
Value string
|
||||||
|
Size float64
|
||||||
|
Font *text.GoTextFaceSource
|
||||||
|
Color color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Text) Render(containerImg *ebiten.Image, pos image.Point) {
|
||||||
|
textImg := t.GetTextImg()
|
||||||
|
|
||||||
|
menuImgOp := &ebiten.DrawImageOptions{}
|
||||||
|
menuImgOp.GeoM.Translate(float64(pos.X), float64(pos.Y))
|
||||||
|
containerImg.DrawImage(textImg, menuImgOp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Text) GetTextImg() *ebiten.Image {
|
||||||
|
img := ebiten.NewImage(200, 50)
|
||||||
|
|
||||||
|
fontOptions := &text.DrawOptions{}
|
||||||
|
fontOptions.ColorScale.ScaleWithColor(t.Color)
|
||||||
|
|
||||||
|
text.Draw(
|
||||||
|
img,
|
||||||
|
t.Value,
|
||||||
|
&text.GoTextFace{
|
||||||
|
Source: t.Font,
|
||||||
|
Size: t.Size,
|
||||||
|
},
|
||||||
|
fontOptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
return img
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
|||||||
|
module github.com/UnintendedFraud/snake-game
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
|
||||||
|
require github.com/hajimehoshi/ebiten/v2 v2.7.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8 // indirect
|
||||||
|
github.com/ebitengine/hideconsole v1.0.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.7.0 // indirect
|
||||||
|
github.com/go-text/typesetting v0.1.1-0.20240325125605-c7936fe59984 // indirect
|
||||||
|
github.com/jezek/xgb v1.1.1 // indirect
|
||||||
|
golang.org/x/image v0.15.0 // indirect
|
||||||
|
golang.org/x/sync v0.6.0 // indirect
|
||||||
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8 h1:5e8X7WEdOWrjrKvgaWF6PRnDvJicfrkEnwAkWtMN74g=
|
||||||
|
github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8/go.mod h1:tWboRRNagZwwwis4QIgEFG1ZNFwBJ3LAhSLAXAAxobQ=
|
||||||
|
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
|
||||||
|
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
|
||||||
|
github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
|
||||||
|
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||||
|
github.com/go-text/typesetting v0.1.1-0.20240325125605-c7936fe59984 h1:NwCC36eQsDf1xVZG9jD7ngXNNjsvk8KXky15ogA1Vo0=
|
||||||
|
github.com/go-text/typesetting v0.1.1-0.20240325125605-c7936fe59984/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I=
|
||||||
|
github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY=
|
||||||
|
github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||||
|
github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
|
||||||
|
github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.7.3 h1:lDpj8KbmmjzwD19rsjXNkyelicu0XGvklZW6/tjrgNs=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.7.3/go.mod h1:1vjyPw+h3n30rfTOpIsbWRXSxZ0Oz1cYc6Tq/2DKoQg=
|
||||||
|
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||||
|
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||||
|
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||||
|
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/UnintendedFraud/snake-game/components"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
game := components.InitGame()
|
||||||
|
|
||||||
|
if err := ebiten.RunGame(game); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Snake game
|
||||||
|
|
||||||
|
Trying to create a small snake game using [https://ebitengine.org/](https://ebitengine.org/).
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
func GetCenter(x, y int) image.Point {
|
||||||
|
return image.Point{
|
||||||
|
X: x / 2,
|
||||||
|
Y: y / 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func SliceIndexOf[T any](
|
||||||
|
s []T,
|
||||||
|
fn func(T) bool,
|
||||||
|
) (int, error) {
|
||||||
|
for i, el := range s {
|
||||||
|
if fn(el) {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1, fmt.Errorf("failed to find a matching element")
|
||||||
|
}
|
||||||
|
|
||||||
|
func SliceContains[T comparable](s []T, el T) bool {
|
||||||
|
for _, element := range s {
|
||||||
|
if element == el {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func SliceContainsCustom[T comparable](s []T, fn func(el T) bool) bool {
|
||||||
|
for _, el := range s {
|
||||||
|
if fn(el) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
type complexStruct struct {
|
||||||
|
field string
|
||||||
|
value int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_IndexOf(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
idx int
|
||||||
|
errIsNull bool
|
||||||
|
}
|
||||||
|
type example[T any] struct {
|
||||||
|
name string
|
||||||
|
s []T
|
||||||
|
searchFn func(T) bool
|
||||||
|
expected expected
|
||||||
|
}
|
||||||
|
|
||||||
|
strSlice := []string{"one", "two", "three"}
|
||||||
|
complexSlice := []complexStruct{
|
||||||
|
{field: "test", value: 12},
|
||||||
|
{field: "something", value: 333},
|
||||||
|
{field: "else", value: 3432},
|
||||||
|
{field: "foo", value: 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
examples := []example[string]{
|
||||||
|
{
|
||||||
|
name: "valid index in slice of strings",
|
||||||
|
s: strSlice,
|
||||||
|
searchFn: func(s string) bool { return s == "three" },
|
||||||
|
expected: expected{idx: 2, errIsNull: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid index in slice of strings",
|
||||||
|
s: strSlice,
|
||||||
|
searchFn: func(s string) bool { return s == "invalid" },
|
||||||
|
expected: expected{idx: -1, errIsNull: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cplxExamples := []example[complexStruct]{
|
||||||
|
{
|
||||||
|
name: "valid index in slice of custom struct",
|
||||||
|
s: complexSlice,
|
||||||
|
searchFn: func(el complexStruct) bool { return el.value == 333 },
|
||||||
|
expected: expected{idx: 1, errIsNull: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid index in slice of custom struct",
|
||||||
|
s: complexSlice,
|
||||||
|
searchFn: func(el complexStruct) bool { return el.field == "invalid" },
|
||||||
|
expected: expected{idx: -1, errIsNull: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ex := range examples {
|
||||||
|
idx, err := SliceIndexOf(ex.s, ex.searchFn)
|
||||||
|
|
||||||
|
if idx != ex.expected.idx {
|
||||||
|
t.Errorf("%s: expected index [%d] but got [%d]", ex.name, ex.expected.idx, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
errIsNull := err == nil
|
||||||
|
if errIsNull != ex.expected.errIsNull {
|
||||||
|
t.Errorf("%s: expected nil error [%t] but got [%t]", ex.name, ex.expected.errIsNull, errIsNull)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ex := range cplxExamples {
|
||||||
|
idx, err := SliceIndexOf(ex.s, ex.searchFn)
|
||||||
|
|
||||||
|
if idx != ex.expected.idx {
|
||||||
|
t.Errorf("%s: expected index [%d] but got [%d]", ex.name, ex.expected.idx, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
errIsNull := err == nil
|
||||||
|
if errIsNull != ex.expected.errIsNull {
|
||||||
|
t.Errorf("%s: expected nil error [%t] but got [%t]", ex.name, ex.expected.errIsNull, errIsNull)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Contains(t *testing.T) {
|
||||||
|
type examples[T comparable] struct {
|
||||||
|
s []T
|
||||||
|
values []T
|
||||||
|
expected []bool
|
||||||
|
}
|
||||||
|
|
||||||
|
examples1 := examples[int]{
|
||||||
|
s: []int{2, 3, 432, 123, 21},
|
||||||
|
values: []int{432, 111},
|
||||||
|
expected: []bool{true, false},
|
||||||
|
}
|
||||||
|
examples2 := examples[string]{
|
||||||
|
s: []string{"one", "two", "three"},
|
||||||
|
values: []string{"invalid", "one"},
|
||||||
|
expected: []bool{false, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range examples1.values {
|
||||||
|
expected := examples1.expected[i]
|
||||||
|
|
||||||
|
if SliceContains(examples1.s, v) != expected {
|
||||||
|
t.Errorf("Test_Contains - [%v] being in %+v should be %t", v, examples1.s, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range examples2.values {
|
||||||
|
expected := examples2.expected[i]
|
||||||
|
|
||||||
|
if SliceContains(examples2.s, v) != expected {
|
||||||
|
t.Errorf("Test_Contains - [%v] being in %+v should be %t", v, examples2.s, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user