1
0
Fork 0

feat: refactor gameplay logic from main.go to a state machine handler.

This commit is contained in:
Henrik Hautakoski 2025-09-21 12:16:42 +02:00
parent 75b4981566
commit 29225b7007
2 changed files with 139 additions and 110 deletions

View file

@ -0,0 +1,130 @@
package handlers
import (
"fmt"
"image/color"
"tetris/assets"
"tetris/engine/audio"
"tetris/engine/core"
"tetris/engine/render"
"tetris/game"
"tetris/game/draw"
"tetris/game/state"
rl "github.com/gen2brain/raylib-go/raylib"
)
type GamePlay struct {
shape game.Shape
shape_pos core.Vec2i8
score game.Score
dropTimer core.IntervalTimer
moveTimer core.IntervalTimer
grid game.Grid
nextShape game.Shape
shapeQueue *game.ShapeQueue
r draw.Renderer
}
func NewGamePlay() *GamePlay {
return &GamePlay{
dropTimer: core.NewIntervalTimer(0.3),
moveTimer: core.NewIntervalTimer(0.1),
r: draw.Renderer{
Theme: &draw.Theme{
FrameBG: color.RGBA{R: 30, G: 30, B: 46, A: 255},
FrameBorder: color.RGBA{R: 242, G: 205, B: 205, A: 255},
TextHeader: color.RGBA{R: 242, G: 205, B: 205, A: 255},
Text: color.RGBA{R: 205, G: 214, B: 244, A: 255},
GridBackground: color.RGBA{R: 17, G: 17, B: 27, A: 255},
},
},
}
}
func (gp *GamePlay) Enter() {
gp.shapeQueue = game.NewShapeQueue()
gp.nextShape = gp.shapeQueue.Next()
gp.SpawnShape()
}
func (GamePlay) Exit() {
}
func (gp *GamePlay) SpawnShape() {
gp.shape = gp.nextShape
gp.nextShape = gp.shapeQueue.Next()
gp.shape_pos = core.Vec2i8{X: 4, Y: 0}
}
func (gp *GamePlay) LockShape() {
audio.Play(assets.SFX_SHAPE_LOCKED)
for _, block := range gp.shape.Coordinates() {
block = gp.shape_pos.Add(block)
// Check bounds
if block.X < 0 || block.X > int8(gp.grid.Width()) || block.Y < 0 || block.Y > int8(gp.grid.Height()) {
continue
}
gp.grid.Set(byte(block.X), byte(block.Y), gp.shape.GetBlock())
}
}
func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) {
if rl.IsKeyPressed(rl.KeyDown) {
gp.dropTimer.SetInterval(0.05)
} else if rl.IsKeyReleased(rl.KeyDown) {
gp.dropTimer.SetInterval(0.3)
}
if rl.IsKeyPressed(rl.KeyUp) {
rotated := gp.shape.RotateCW()
if !game.CheckShapeCollision(gp.shape_pos, &rotated, &gp.grid) {
gp.shape = rotated
}
}
if gp.moveTimer.UpdateReset(delta) && (rl.IsKeyDown(rl.KeyLeft) || rl.IsKeyDown(rl.KeyRight)) {
new_pos := gp.shape_pos
if rl.IsKeyDown(rl.KeyLeft) {
new_pos.X -= 1
} else {
new_pos.X += 1
}
if !game.CheckShapeCollision(new_pos, &gp.shape, &gp.grid) {
gp.shape_pos.X = new_pos.X
}
}
if gp.dropTimer.UpdateReset(delta) {
new_pos := gp.shape_pos
new_pos.Y += 1
// Update position if it does not collide
if game.CheckShapeCollision(new_pos, &gp.shape, &gp.grid) {
gp.LockShape()
num_rows := gp.grid.ClearFullRows()
if num_rows > 0 {
audio.Play(assets.SFX_ROW_CLEARED)
gp.score.Lines(num_rows)
}
gp.SpawnShape()
} else {
gp.shape_pos = new_pos
}
}
}
func (gp GamePlay) Render() {
render.Begin(gp.r.Theme.GridBackground)
gp.r.DrawGrid(rl.NewVector2(25, 25), gp.grid)
draw.DrawShape(rl.NewVector2(25, 25), gp.shape_pos, gp.shape)
gp.r.DrawFrame(rl.RectangleInt32{X: 400, Y: 25, Width: 250, Height: 100})
gp.r.DrawHeaderText(410, 30, "Score")
gp.r.DrawText(410, 65, fmt.Sprintf("%.7d", gp.score))
gp.r.DrawFrame(rl.RectangleInt32{X: 400, Y: 150, Width: 250, Height: 200})
gp.r.DrawHeaderText(410, 155, "Next")
draw.DrawShape(rl.NewVector2(450, 150), core.NewVec2[int8](1, 3), gp.nextShape)
render.End()
}

119
main.go
View file

@ -1,120 +1,17 @@
package main
import (
"fmt"
"image/color"
"tetris/assets"
"tetris/engine/audio"
"tetris/engine/core"
"tetris/engine/graphics"
"tetris/engine/render"
"tetris/game"
"tetris/game/draw"
"tetris/game/state/handlers"
"tetris/game/state/machine"
rl "github.com/gen2brain/raylib-go/raylib"
)
var (
shape game.Shape
shape_pos core.Vec2i8
score game.Score
dropTimer = core.NewIntervalTimer(0.3)
moveTimer = core.NewIntervalTimer(0.1)
grid = game.Grid{}
nextShape = game.NewShape(game.SHAPE_O)
shapeQueue = game.NewShapeQueue()
r = draw.Renderer{
Theme: &draw.Theme{
FrameBG: color.RGBA{R: 30, G: 30, B: 46, A: 255},
FrameBorder: color.RGBA{R: 242, G: 205, B: 205, A: 255},
TextHeader: color.RGBA{R: 242, G: 205, B: 205, A: 255},
Text: color.RGBA{R: 205, G: 214, B: 244, A: 255},
GridBackground: color.RGBA{R: 17, G: 17, B: 27, A: 255},
},
}
)
func SpawnShape() {
shape = nextShape
nextShape = shapeQueue.Next()
shape_pos = core.Vec2i8{X: 4, Y: 0}
}
func LockShape() {
audio.Play(assets.SFX_SHAPE_LOCKED)
for _, block := range shape.Coordinates() {
block = shape_pos.Add(block)
// Check bounds
if block.X < 0 || block.X > int8(grid.Width()) || block.Y < 0 || block.Y > int8(grid.Height()) {
continue
}
grid.Set(byte(block.X), byte(block.Y), shape.GetBlock())
}
}
func Update(delta float32) {
if rl.IsKeyPressed(rl.KeyDown) {
dropTimer.SetInterval(0.05)
} else if rl.IsKeyReleased(rl.KeyDown) {
dropTimer.SetInterval(0.3)
}
if rl.IsKeyPressed(rl.KeyUp) {
rotated := shape.RotateCW()
if !game.CheckShapeCollision(shape_pos, &rotated, &grid) {
shape = rotated
}
}
if moveTimer.UpdateReset(delta) && (rl.IsKeyDown(rl.KeyLeft) || rl.IsKeyDown(rl.KeyRight)) {
new_pos := shape_pos
if rl.IsKeyDown(rl.KeyLeft) {
new_pos.X -= 1
} else {
new_pos.X += 1
}
if !game.CheckShapeCollision(new_pos, &shape, &grid) {
shape_pos.X = new_pos.X
}
}
if dropTimer.UpdateReset(delta) {
new_pos := shape_pos
new_pos.Y += 1
// Update position if it does not collide
if game.CheckShapeCollision(new_pos, &shape, &grid) {
LockShape()
num_rows := grid.ClearFullRows()
if num_rows > 0 {
audio.Play(assets.SFX_ROW_CLEARED)
score.Lines(num_rows)
}
SpawnShape()
} else {
shape_pos = new_pos
}
}
}
func Render() {
render.Begin(r.Theme.GridBackground)
r.DrawGrid(rl.NewVector2(25, 25), grid)
draw.DrawShape(rl.NewVector2(25, 25), shape_pos, shape)
r.DrawFrame(rl.RectangleInt32{X: 400, Y: 25, Width: 250, Height: 100})
r.DrawHeaderText(410, 30, "Score")
r.DrawText(410, 65, fmt.Sprintf("%.7d", score))
r.DrawFrame(rl.RectangleInt32{X: 400, Y: 150, Width: 250, Height: 200})
r.DrawHeaderText(410, 155, "Next")
draw.DrawShape(rl.NewVector2(450, 150), core.NewVec2[int8](1, 3), nextShape)
render.End()
}
func main() {
// Set random blocks to test
render.Init(render.Config{
Title: "Tetris",
WindowWidth: 685,
@ -136,13 +33,15 @@ func main() {
render.SetTexture(texture)
render.SetFont(&assets.Font)
nextShape = shapeQueue.Next()
SpawnShape()
// Setup state machine.
fsm := machine.New()
fsm.Register("gameplay", handlers.NewGamePlay())
fsm.Start("gameplay")
// Enter game loop
for !rl.WindowShouldClose() {
audio.Update()
Update(rl.GetFrameTime())
Render()
fsm.Update(rl.GetFrameTime())
fsm.Render()
}
}