From a5da01bdb30910cb8d5d762b25f4a11fc81638f6 Mon Sep 17 00:00:00 2001 From: Henrik Hautakoski Date: Fri, 17 Jul 2026 11:10:53 +0200 Subject: [PATCH 1/3] chore: fix unicode character in comment --- game/score.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/score.go b/game/score.go index ac00af7..d958059 100644 --- a/game/score.go +++ b/game/score.go @@ -5,7 +5,7 @@ package game const SCORE_MAX Score = 9999999 // Score is a running total of points earned. -// It’s a distinct type (over uint32) to prevent mixing raw integers with scores. +// It's a distinct type (over uint32) to prevent mixing raw integers with scores. type Score uint32 // Lines adds points for clearing a number of lines at once. From b6ec52a3d921f38e8f87ece9a0e7812e71385cc4 Mon Sep 17 00:00:00 2001 From: Henrik Hautakoski Date: Fri, 28 Aug 2026 11:35:50 +0200 Subject: [PATCH 2/3] chore: fix line endings --- engine/graphics/image.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/engine/graphics/image.go b/engine/graphics/image.go index d92b93c..26ef9cb 100644 --- a/engine/graphics/image.go +++ b/engine/graphics/image.go @@ -1,7 +1,7 @@ -package graphics - -import rl "github.com/gen2brain/raylib-go/raylib" - -func LoadImageFromMemory(typ string, data []byte) *rl.Image { - return rl.LoadImageFromMemory(typ, data, int32(len(data))) -} +package graphics + +import rl "github.com/gen2brain/raylib-go/raylib" + +func LoadImageFromMemory(typ string, data []byte) *rl.Image { + return rl.LoadImageFromMemory(typ, data, int32(len(data))) +} From cc8fa88afeef1c6c34c336eaff365daec68fbb4f Mon Sep 17 00:00:00 2001 From: Henrik Hautakoski Date: Mon, 31 Aug 2026 14:05:42 +0200 Subject: [PATCH 3/3] feat: add ghost piece --- README.md | 3 +- assets/sprite.xcf | Bin 36247 -> 37865 bytes assets/sprites.png | Bin 1209 -> 1242 bytes game/block.go | 12 ++++++- game/config/config.go | 2 ++ game/draw/shape.go | 4 ++- game/ghost_shape.go | 39 ++++++++++++++++++++ game/line_clear_animation.go | 8 +++-- game/shape.go | 8 +++++ game/state/handlers/gameplay.go | 25 +++++++++++-- game/state/handlers/options.go | 10 ++++-- game/ui/widgets/checkbox.go | 62 ++++++++++++++++++++++++++++++++ 12 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 game/ghost_shape.go create mode 100644 game/ui/widgets/checkbox.go diff --git a/README.md b/README.md index a1ff326..f7efed0 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ go build tetris.go ## Roadmap - [x] Line clear animations -- [ ] Hard drop and ghost piece +- [x] ghost piece +- [ ] Hard drop - [ ] Level/speed progression and pause - [ ] High score persistence - [ ] Custom key bindings diff --git a/assets/sprite.xcf b/assets/sprite.xcf index 982b2722e2e337b9553bd2050031d340fcaadbd5..e376963412c6f406d6395dbd0485f9810aee155c 100644 GIT binary patch delta 4450 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 diff --git a/assets/sprites.png b/assets/sprites.png index 4b350bd9de20af82bd9834870d4d5919f8ac0342..b26a3b6fdc68e419ecd1e548eff86ea605cbad4c 100644 GIT binary patch delta 1121 zcmdnVd5d#GNBt`f8BRs(wAoUV^m`2WJ-@4JXkhabBA-=AzT{c!EW+WH?^%KwvjzH7GsJ~l<(_Wq6cLVEvs z%=aeT$~XRhlxb)2469#%s_UaZd3&7HS5&Rn)>o-ty??@=wR4caZu#5&pEs@M@ZV#y>?`cNlyAoiCX2ZSnpobv z5^*KJ^&4-lW%CuirT13W`p4EHvY^-6JCtifbII{+j@oyI{lZl%@99tU z&lRvNZHV0-+<$V?lm%;LTrV=uFRVTl zdvjlr`tuUSixai)ztDUTFh?OQXx*Z>qB7a*>nDr;+P%uUeDa>p!3@v;XzGd_J=QOI zAbriUR{~cny~8)W7kN0_dGeF@Dy$iHekW7E+W2xV-5vFDj;K#*mgh!K=YM`d4q7#Z z+?ESJxSw6_TW_xtzn`14S2_2&$lMiv`g_^7Id6FQuGGL(B(NwVIG<}r`PVt$Iab=; z`Dt1?@yfIMr9t<%XL9Clu-N+dm&Bai6`p}DNn38n9lk!VivQx5REck2b>{3UO4`}K z>*qzABOFhUPs#DHU&p*{D`)Oji7QXKC12LseP`v}I>oD2c8RX3w|u3H)0Y>EmmZgy zBH(4|oqbRH$cg@`^H=gsw2FNYFu%2ZoxbGN6;7L81+DHC7MvBucXX1;w*~%;Q*N8B zb5vcQqPo3z(wC!qdrc&G*qRR}7!am1r2jLrE?&6g^Qy4b3=9kmp00i_>zopr0I^0S A?*IS* delta 1088 zcmcb`xs!83NBv6DHGGGLw9_Yp=I`eDm&>v;Xf3y{~xt^SIpY z*Kb~&^lap4cURxB^WS6L-ygo22F*PCe8wDG-)-~HKePDvRqO7*`l7bY&nJKR=KtJc zuchDi|F*|pKILxyUzb$$?aYJyl6*Rkrt`QfGaN{=X**rK<7LRz2^T-uG9>Jop-?Vw zSnBn={P5Pl*I7UL^Id-LmufGQck6#S&-0=k`n$gK-_Lnd-}BJ!dBOcD-xg1o{vUhj z>GOii+)MwP{f*a|VBNlQ^7s0LdeQwVp?@ZYeBA!6^HKio`5OPO*#-WI{=c-d{zc_X0`M99^pD}A}Lj14Z`<}C+cc0y_ zH_2arpPK!O>+khUt`+7xG`bf3`&7le`uq=>sP)SAA!;vkR@`~DVOiah2~qAZCkGrT z-^9Dd+`>#tMfRMT$DG`y>n~n%kNzbRzn3Ft)}z~H^URL6yqdUd)^V{8Pey^_01ehH zn-mO}Fr3`AaD9TuSMQte>@FWzaIfLQ72UJ*uQI$$Ub1CwP|e$^S7N`$W<~#+XB*|s zc=hPw6t3^3CT3ans*BaHek|RcHNotvY}Df)X&&DHIbOb#Q7E0i=BD%ZT={5)taFUJ z^!oo!n^#=2k8Q$}Hv#jqweoMn;akRGOhViX`TemVyy(9l3MKI$3w)fFT zGR}Q_UFxA{onhzZ$sD}v*oUd@?>Op9J{snT^Oz{BPx-y1d;W|ch7)Ul8m8U8VwcPC zZ7auH;}u8T4+^C(uRGr_JKJly&)Kfn>gx{GscUp^$DL=~WwD3T_u$6$skb!4qW1gW zx$tqbi9*uu?%yXIOGQlYKHU5+Sv9!i#ZNnN*Ga99o%j4qzQ^jZZ(FA9tRnBQ&8Igk sS#KHf_fMZchhmF>6AofR>}PqF`(h=lqZ@WJFfcH9y85}Sb4q9e0D!_N^Z)<= diff --git a/game/block.go b/game/block.go index 0221d49..5b7445a 100644 --- a/game/block.go +++ b/game/block.go @@ -16,12 +16,22 @@ const ( BLOCK_ORANGE BLOCK_MAGENTA BLOCK_TEAL + BLOCK_GHOST ) func (c Block) Tile() graphics.Tile { - if c >= 8 { + if c >= 9 { c = BLOCK_EMPTY } + + if c == BLOCK_GHOST { + return graphics.Tile{ + Size: assets.BLOCK_TILE_SIZE, + X: byte(7), + Y: byte(1), + } + } + return graphics.Tile{ Size: assets.BLOCK_TILE_SIZE, X: byte(c), diff --git a/game/config/config.go b/game/config/config.go index 82ad3d8..7437eae 100644 --- a/game/config/config.go +++ b/game/config/config.go @@ -9,11 +9,13 @@ import ( type Config struct { fd *os.File SoundVolume byte `json:"sound_volume"` + GhostPiece bool `json:"ghost_piece"` } func DefaultConfig(fd *os.File) *Config { cfg := Config{ SoundVolume: 255, + GhostPiece: false, } cfg.fd = fd return &cfg diff --git a/game/draw/shape.go b/game/draw/shape.go index 8913b41..08847df 100644 --- a/game/draw/shape.go +++ b/game/draw/shape.go @@ -14,8 +14,10 @@ type Shape interface { } func DrawShape(offset rl.Vector2, pos core.Vec2i8, shape Shape) { - tile := shape.Tile() + DrawShapeTile(offset, pos, shape, shape.Tile()) +} +func DrawShapeTile(offset rl.Vector2, pos core.Vec2i8, shape Shape, tile graphics.Tile) { for _, coord := range shape.Coordinates() { block := pos.Add(coord) grid.DrawBlock(offset, byte(block.X), byte(block.Y), tile) diff --git a/game/ghost_shape.go b/game/ghost_shape.go new file mode 100644 index 0000000..1a2a96e --- /dev/null +++ b/game/ghost_shape.go @@ -0,0 +1,39 @@ +package game + +import ( + "tetris/engine/core" + "tetris/game/draw" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +type GhostShape struct { + posY int8 // only need Y coordinate +} + +func (gs *GhostShape) UpdatePos(shape *Shape, pos core.Vec2i8, grid *Grid) { + for gs.posY = pos.Y; pos.Y <= int8(grid.Height()); pos.Y++ { + if CheckShapeCollision(pos, shape, grid) { + gs.posY = pos.Y - 1 + break + } + } +} + +func (gs *GhostShape) Draw(offset rl.Vector2, shape *Shape, pos core.Vec2i8) { + draw.DrawShapeTile(offset, core.NewVec2(pos.X, gs.posY), shape, BLOCK_GHOST.Tile()) +} + +func UpdateGhostShapeY(shape *Shape, pos core.Vec2i8, grid *Grid) int8 { + for ; pos.Y <= int8(grid.Height()); pos.Y++ { + if CheckShapeCollision(pos, shape, grid) { + pos.Y = pos.Y - 1 + break + } + } + return pos.Y +} + +func DrawGhost(offset rl.Vector2, shape *Shape, pos core.Vec2i8, ghost_y int8) { + draw.DrawShapeTile(offset, core.NewVec2(pos.X, ghost_y), shape, BLOCK_GHOST.Tile()) +} diff --git a/game/line_clear_animation.go b/game/line_clear_animation.go index 5c650d5..d79ff8a 100644 --- a/game/line_clear_animation.go +++ b/game/line_clear_animation.go @@ -32,15 +32,15 @@ func (lca *LineClearAnimation) SetLines(rows []byte) { } } -func (lca *LineClearAnimation) Update(grid *Grid) { +func (lca *LineClearAnimation) Update(grid *Grid) bool { if lca.Completed() { - return + return false } if lca.leftIndex < 0 || lca.rightIndex > GRID_WIDTH { grid.MoveLinesDown(lca.lines...) lca.Reset() - return + return true } for _, lineIndex := range lca.lines { @@ -50,6 +50,8 @@ func (lca *LineClearAnimation) Update(grid *Grid) { lca.leftIndex-- lca.rightIndex++ + + return false } func (lca LineClearAnimation) Completed() bool { diff --git a/game/shape.go b/game/shape.go index 7543e39..c6cccfd 100644 --- a/game/shape.go +++ b/game/shape.go @@ -58,6 +58,10 @@ func (s Shape) Coordinates() [4]core.Vec2i8 { } func (s Shape) RotateCW() Shape { + if s.typ == SHAPE_O { + return s + } + rotated := [3]core.Vec2i8{} for i, block := range s.coordinates { @@ -71,6 +75,10 @@ func (s Shape) RotateCW() Shape { } func (s Shape) RotateCCW() Shape { + if s.typ == SHAPE_O { + return s + } + rotated := [3]core.Vec2i8{} for i, block := range s.coordinates { diff --git a/game/state/handlers/gameplay.go b/game/state/handlers/gameplay.go index b65da11..7730635 100644 --- a/game/state/handlers/gameplay.go +++ b/game/state/handlers/gameplay.go @@ -18,6 +18,8 @@ import ( type GamePlay struct { shape game.Shape shape_pos core.Vec2i8 + ghost game.GhostShape + ghost_needs_update bool score game.Score dropTimer core.IntervalTimer moveTimer core.IntervalTimer @@ -66,6 +68,7 @@ func (gp *GamePlay) SpawnShape() { gp.shape = gp.nextShape gp.nextShape = gp.shapeQueue.Next() gp.shape_pos = core.Vec2i8{X: 4, Y: 0} + gp.ghost_needs_update = true } func (gp *GamePlay) LockShape() { @@ -90,7 +93,9 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) { if !gp.lineClearAnimation.Completed() { if gp.lineClearTimer.UpdateReset(delta) { - gp.lineClearAnimation.Update(&gp.grid) + if gp.lineClearAnimation.Update(&gp.grid) { + gp.ghost_needs_update = true + } } return } @@ -99,6 +104,7 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) { rotated := gp.shape.RotateCW() if !game.CheckShapeCollision(gp.shape_pos, &rotated, &gp.grid) { gp.shape = rotated + gp.ghost_needs_update = true } } @@ -111,6 +117,7 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) { } if !game.CheckShapeCollision(new_pos, &gp.shape, &gp.grid) { gp.shape_pos.X = new_pos.X + gp.ghost_needs_update = true } } @@ -136,12 +143,24 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) { gp.shape_pos = new_pos } } + + if gp.ghost_needs_update { + if game.Config.GhostPiece { + gp.ghost.UpdatePos(&gp.shape, gp.shape_pos, &gp.grid) + } + gp.ghost_needs_update = false + } } func (gp GamePlay) Render() { + offset := rl.NewVector2(25, 25) + 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.DrawGrid(offset, gp.grid) + draw.DrawShape(offset, gp.shape_pos, gp.shape) + if game.Config.GhostPiece && gp.lineClearAnimation.Completed() { + gp.ghost.Draw(offset, &gp.shape, gp.shape_pos) + } 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)) diff --git a/game/state/handlers/options.go b/game/state/handlers/options.go index 8fabd6d..9456389 100644 --- a/game/state/handlers/options.go +++ b/game/state/handlers/options.go @@ -16,8 +16,9 @@ import ( ) type OptionsMenu struct { - sndVolume *widgets.Slider - list *layouts.ListBox + sndVolume *widgets.Slider + ghostBlock *widgets.Checkbox + list *layouts.ListBox } func soundVolumeChanged(widget *widgets.Slider) { @@ -33,10 +34,15 @@ func soundVolumeChanged(widget *widgets.Slider) { func NewOptionsMenu() *OptionsMenu { sndVolume := widgets.NewSlider(0, 255, 10).WithOnChange(soundVolumeChanged) + ghostPiece := widgets.NewCheckbox(game.Config.GhostPiece, func(value bool) { + game.Config.GhostPiece = value + game.Config.Save() + }) return &OptionsMenu{ sndVolume: sndVolume, list: layouts.NewListBox([]ui.InputWidget{ layouts.NewInputControl(widgets.NewLabel("Sound Volume", 16), sndVolume), + layouts.NewInputControl(widgets.NewLabel("Show ghost piece", 16), ghostPiece), }).Spacing(10).OnSelect(uievents.MenuSelect), } } diff --git a/game/ui/widgets/checkbox.go b/game/ui/widgets/checkbox.go new file mode 100644 index 0000000..5954c25 --- /dev/null +++ b/game/ui/widgets/checkbox.go @@ -0,0 +1,62 @@ +package widgets + +import ( + "tetris/engine/core" + "tetris/engine/input" + "tetris/engine/render" + "tetris/game/ui/base" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +type CheckboxAction func(value bool) + +type Checkbox struct { + *base.WithFocus + action CheckboxAction + value bool +} + +func NewCheckbox(value bool, action CheckboxAction) *Checkbox { + return &Checkbox{ + WithFocus: &base.WithFocus{}, + value: value, + action: action, + } +} + +func (cb Checkbox) Size() core.Vec2i { + return core.NewVec2(24, 24) +} + +func (cb *Checkbox) HandleInput() { + if input.KeyPressedWithRepeat(rl.KeyEnter) { + cb.value = !cb.value + cb.action(cb.value) + } +} + +func (cb Checkbox) Draw(x, y int32) { + size := cb.Size() + space := int32(2) + color := rl.White + if cb.HasFocus() { + color = rl.Red + } + + render.DrawRectOutlineBorder(rl.RectangleInt32{ + X: x, + Y: y, + Width: int32(size.X), + Height: int32(size.Y), + }, 4, color) + + if cb.value { + render.DrawRectBorder(rl.RectangleInt32{ + X: x + space, + Y: y + space, + Width: int32(size.X) - (space * 2), + Height: int32(size.Y) - (space * 2), + }, color, 0, rl.Blank) + } +}