From 29225b7007cd154243c48bad8c321b6c4633f323 Mon Sep 17 00:00:00 2001 From: Henrik Hautakoski Date: Sun, 21 Sep 2025 12:16:42 +0200 Subject: [PATCH] feat: refactor gameplay logic from main.go to a state machine handler. --- game/state/handlers/gameplay.go | 130 ++++++++++++++++++++++++++++++++ main.go | 119 +++-------------------------- 2 files changed, 139 insertions(+), 110 deletions(-) create mode 100644 game/state/handlers/gameplay.go diff --git a/game/state/handlers/gameplay.go b/game/state/handlers/gameplay.go new file mode 100644 index 0000000..9d85525 --- /dev/null +++ b/game/state/handlers/gameplay.go @@ -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() +} diff --git a/main.go b/main.go index 881c145..9fbe5e9 100644 --- a/main.go +++ b/main.go @@ -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() } }