Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc8fa88afe | |||
| b6ec52a3d9 | |||
| a5da01bdb3 |
14 changed files with 170 additions and 19 deletions
|
|
@ -65,7 +65,8 @@ go build tetris.go
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [x] Line clear animations
|
- [x] Line clear animations
|
||||||
- [ ] Hard drop and ghost piece
|
- [x] ghost piece
|
||||||
|
- [ ] Hard drop
|
||||||
- [ ] Level/speed progression and pause
|
- [ ] Level/speed progression and pause
|
||||||
- [ ] High score persistence
|
- [ ] High score persistence
|
||||||
- [ ] Custom key bindings
|
- [ ] Custom key bindings
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,7 +1,7 @@
|
||||||
package graphics
|
package graphics
|
||||||
|
|
||||||
import rl "github.com/gen2brain/raylib-go/raylib"
|
import rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
|
|
||||||
func LoadImageFromMemory(typ string, data []byte) *rl.Image {
|
func LoadImageFromMemory(typ string, data []byte) *rl.Image {
|
||||||
return rl.LoadImageFromMemory(typ, data, int32(len(data)))
|
return rl.LoadImageFromMemory(typ, data, int32(len(data)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,22 @@ const (
|
||||||
BLOCK_ORANGE
|
BLOCK_ORANGE
|
||||||
BLOCK_MAGENTA
|
BLOCK_MAGENTA
|
||||||
BLOCK_TEAL
|
BLOCK_TEAL
|
||||||
|
BLOCK_GHOST
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c Block) Tile() graphics.Tile {
|
func (c Block) Tile() graphics.Tile {
|
||||||
if c >= 8 {
|
if c >= 9 {
|
||||||
c = BLOCK_EMPTY
|
c = BLOCK_EMPTY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c == BLOCK_GHOST {
|
||||||
|
return graphics.Tile{
|
||||||
|
Size: assets.BLOCK_TILE_SIZE,
|
||||||
|
X: byte(7),
|
||||||
|
Y: byte(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return graphics.Tile{
|
return graphics.Tile{
|
||||||
Size: assets.BLOCK_TILE_SIZE,
|
Size: assets.BLOCK_TILE_SIZE,
|
||||||
X: byte(c),
|
X: byte(c),
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ import (
|
||||||
type Config struct {
|
type Config struct {
|
||||||
fd *os.File
|
fd *os.File
|
||||||
SoundVolume byte `json:"sound_volume"`
|
SoundVolume byte `json:"sound_volume"`
|
||||||
|
GhostPiece bool `json:"ghost_piece"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig(fd *os.File) *Config {
|
func DefaultConfig(fd *os.File) *Config {
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
SoundVolume: 255,
|
SoundVolume: 255,
|
||||||
|
GhostPiece: false,
|
||||||
}
|
}
|
||||||
cfg.fd = fd
|
cfg.fd = fd
|
||||||
return &cfg
|
return &cfg
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ type Shape interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
func DrawShape(offset rl.Vector2, pos core.Vec2i8, shape Shape) {
|
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() {
|
for _, coord := range shape.Coordinates() {
|
||||||
block := pos.Add(coord)
|
block := pos.Add(coord)
|
||||||
grid.DrawBlock(offset, byte(block.X), byte(block.Y), tile)
|
grid.DrawBlock(offset, byte(block.X), byte(block.Y), tile)
|
||||||
|
|
|
||||||
39
game/ghost_shape.go
Normal file
39
game/ghost_shape.go
Normal file
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
@ -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() {
|
if lca.Completed() {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if lca.leftIndex < 0 || lca.rightIndex > GRID_WIDTH {
|
if lca.leftIndex < 0 || lca.rightIndex > GRID_WIDTH {
|
||||||
grid.MoveLinesDown(lca.lines...)
|
grid.MoveLinesDown(lca.lines...)
|
||||||
lca.Reset()
|
lca.Reset()
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, lineIndex := range lca.lines {
|
for _, lineIndex := range lca.lines {
|
||||||
|
|
@ -50,6 +50,8 @@ func (lca *LineClearAnimation) Update(grid *Grid) {
|
||||||
|
|
||||||
lca.leftIndex--
|
lca.leftIndex--
|
||||||
lca.rightIndex++
|
lca.rightIndex++
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lca LineClearAnimation) Completed() bool {
|
func (lca LineClearAnimation) Completed() bool {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ package game
|
||||||
const SCORE_MAX Score = 9999999
|
const SCORE_MAX Score = 9999999
|
||||||
|
|
||||||
// Score is a running total of points earned.
|
// 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
|
type Score uint32
|
||||||
|
|
||||||
// Lines adds points for clearing a number of lines at once.
|
// Lines adds points for clearing a number of lines at once.
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ func (s Shape) Coordinates() [4]core.Vec2i8 {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Shape) RotateCW() Shape {
|
func (s Shape) RotateCW() Shape {
|
||||||
|
if s.typ == SHAPE_O {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
rotated := [3]core.Vec2i8{}
|
rotated := [3]core.Vec2i8{}
|
||||||
|
|
||||||
for i, block := range s.coordinates {
|
for i, block := range s.coordinates {
|
||||||
|
|
@ -71,6 +75,10 @@ func (s Shape) RotateCW() Shape {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Shape) RotateCCW() Shape {
|
func (s Shape) RotateCCW() Shape {
|
||||||
|
if s.typ == SHAPE_O {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
rotated := [3]core.Vec2i8{}
|
rotated := [3]core.Vec2i8{}
|
||||||
|
|
||||||
for i, block := range s.coordinates {
|
for i, block := range s.coordinates {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import (
|
||||||
type GamePlay struct {
|
type GamePlay struct {
|
||||||
shape game.Shape
|
shape game.Shape
|
||||||
shape_pos core.Vec2i8
|
shape_pos core.Vec2i8
|
||||||
|
ghost game.GhostShape
|
||||||
|
ghost_needs_update bool
|
||||||
score game.Score
|
score game.Score
|
||||||
dropTimer core.IntervalTimer
|
dropTimer core.IntervalTimer
|
||||||
moveTimer core.IntervalTimer
|
moveTimer core.IntervalTimer
|
||||||
|
|
@ -66,6 +68,7 @@ func (gp *GamePlay) SpawnShape() {
|
||||||
gp.shape = gp.nextShape
|
gp.shape = gp.nextShape
|
||||||
gp.nextShape = gp.shapeQueue.Next()
|
gp.nextShape = gp.shapeQueue.Next()
|
||||||
gp.shape_pos = core.Vec2i8{X: 4, Y: 0}
|
gp.shape_pos = core.Vec2i8{X: 4, Y: 0}
|
||||||
|
gp.ghost_needs_update = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gp *GamePlay) LockShape() {
|
func (gp *GamePlay) LockShape() {
|
||||||
|
|
@ -90,7 +93,9 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) {
|
||||||
|
|
||||||
if !gp.lineClearAnimation.Completed() {
|
if !gp.lineClearAnimation.Completed() {
|
||||||
if gp.lineClearTimer.UpdateReset(delta) {
|
if gp.lineClearTimer.UpdateReset(delta) {
|
||||||
gp.lineClearAnimation.Update(&gp.grid)
|
if gp.lineClearAnimation.Update(&gp.grid) {
|
||||||
|
gp.ghost_needs_update = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +104,7 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) {
|
||||||
rotated := gp.shape.RotateCW()
|
rotated := gp.shape.RotateCW()
|
||||||
if !game.CheckShapeCollision(gp.shape_pos, &rotated, &gp.grid) {
|
if !game.CheckShapeCollision(gp.shape_pos, &rotated, &gp.grid) {
|
||||||
gp.shape = rotated
|
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) {
|
if !game.CheckShapeCollision(new_pos, &gp.shape, &gp.grid) {
|
||||||
gp.shape_pos.X = new_pos.X
|
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
|
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() {
|
func (gp GamePlay) Render() {
|
||||||
|
offset := rl.NewVector2(25, 25)
|
||||||
|
|
||||||
render.Begin(gp.r.Theme.GridBackground)
|
render.Begin(gp.r.Theme.GridBackground)
|
||||||
gp.r.DrawGrid(rl.NewVector2(25, 25), gp.grid)
|
gp.r.DrawGrid(offset, gp.grid)
|
||||||
draw.DrawShape(rl.NewVector2(25, 25), gp.shape_pos, gp.shape)
|
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.DrawFrame(rl.RectangleInt32{X: 400, Y: 25, Width: 250, Height: 100})
|
||||||
gp.r.DrawHeaderText(410, 30, "Score")
|
gp.r.DrawHeaderText(410, 30, "Score")
|
||||||
gp.r.DrawText(410, 65, fmt.Sprintf("%.7d", gp.score))
|
gp.r.DrawText(410, 65, fmt.Sprintf("%.7d", gp.score))
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type OptionsMenu struct {
|
type OptionsMenu struct {
|
||||||
sndVolume *widgets.Slider
|
sndVolume *widgets.Slider
|
||||||
list *layouts.ListBox
|
ghostBlock *widgets.Checkbox
|
||||||
|
list *layouts.ListBox
|
||||||
}
|
}
|
||||||
|
|
||||||
func soundVolumeChanged(widget *widgets.Slider) {
|
func soundVolumeChanged(widget *widgets.Slider) {
|
||||||
|
|
@ -33,10 +34,15 @@ func soundVolumeChanged(widget *widgets.Slider) {
|
||||||
|
|
||||||
func NewOptionsMenu() *OptionsMenu {
|
func NewOptionsMenu() *OptionsMenu {
|
||||||
sndVolume := widgets.NewSlider(0, 255, 10).WithOnChange(soundVolumeChanged)
|
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{
|
return &OptionsMenu{
|
||||||
sndVolume: sndVolume,
|
sndVolume: sndVolume,
|
||||||
list: layouts.NewListBox([]ui.InputWidget{
|
list: layouts.NewListBox([]ui.InputWidget{
|
||||||
layouts.NewInputControl(widgets.NewLabel("Sound Volume", 16), sndVolume),
|
layouts.NewInputControl(widgets.NewLabel("Sound Volume", 16), sndVolume),
|
||||||
|
layouts.NewInputControl(widgets.NewLabel("Show ghost piece", 16), ghostPiece),
|
||||||
}).Spacing(10).OnSelect(uievents.MenuSelect),
|
}).Spacing(10).OnSelect(uievents.MenuSelect),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
game/ui/widgets/checkbox.go
Normal file
62
game/ui/widgets/checkbox.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue