v/examples/tetris/tetris.v

479 lines
11 KiB
V
Raw Normal View History

// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved.
2019-06-23 04:21:30 +02:00
// Use of this source code is governed by an MIT license
2019-08-09 07:28:37 +02:00
// that can be found in the LICENSE file.
2019-09-20 18:05:14 +02:00
module main
import os
2019-06-22 20:20:28 +02:00
import rand
import time
2019-08-09 07:28:37 +02:00
import gx
import gg
import sokol.sapp
2019-06-22 20:20:28 +02:00
const (
block_size = 20 // pixels
field_height = 20 // # of blocks
field_width = 10
tetro_size = 4
win_width = block_size * field_width
win_height = block_size * field_height
timer_period = 250 // ms
text_size = 24
2020-05-22 17:36:09 +02:00
limit_thickness = 3
2019-06-22 20:20:28 +02:00
)
const (
text_cfg = gx.TextCfg{
align: .left
size: text_size
color: gx.rgb(0, 0, 0)
}
2019-10-27 08:24:28 +01:00
over_cfg = gx.TextCfg{
align: .left
size: text_size
color: gx.white
2019-10-27 08:24:28 +01:00
}
)
2019-06-22 20:20:28 +02:00
const (
// Tetros' 4 possible states are encoded in binaries
// 0000 0 0000 0 0000 0 0000 0 0000 0 0000 0
// 0000 0 0000 0 0000 0 0000 0 0011 3 0011 3
// 0110 6 0010 2 0011 3 0110 6 0001 1 0010 2
// 0110 6 0111 7 0110 6 0011 3 0001 1 0010 2
// There is a special case 1111, since 15 can't be used.
b_tetros = [
2019-06-22 20:20:28 +02:00
[66, 66, 66, 66],
[27, 131, 72, 232],
[36, 231, 36, 231],
[63, 132, 63, 132],
[311, 17, 223, 74],
[322, 71, 113, 47],
[1111, 9, 1111, 9],
]
// Each tetro has its unique color
colors = [
gx.rgb(0, 0, 0), /* unused ? */
gx.rgb(255, 242, 0), /* yellow quad */
gx.rgb(174, 0, 255), /* purple triple */
gx.rgb(60, 255, 0), /* green short topright */
gx.rgb(255, 0, 0), /* red short topleft */
gx.rgb(255, 180, 31), /* orange long topleft */
gx.rgb(33, 66, 255), /* blue long topright */
gx.rgb(74, 198, 255), /* lightblue longest */
gx.rgb(0, 170, 170), /* unused ? */
2019-06-22 20:20:28 +02:00
]
background_color = gx.white
ui_color = gx.rgba(255, 0, 0, 210)
2019-06-22 20:20:28 +02:00
)
// TODO: type Tetro [tetro_size]struct{ x, y int }
2019-06-22 20:20:28 +02:00
struct Block {
mut:
2019-06-22 20:20:28 +02:00
x int
y int
}
enum GameState {
paused
running
gameover
}
2019-06-22 20:20:28 +02:00
struct Game {
mut:
2019-08-09 07:28:37 +02:00
// Score of the current game
score int
2020-10-02 15:42:05 +02:00
// Lines of the current game
lines int
// State of the current game
state GameState
2019-06-22 20:20:28 +02:00
// Position of the current tetro
pos_x int
pos_y int
2019-06-22 20:20:28 +02:00
// field[y][x] contains the color of the block with (x,y) coordinates
// "-1" border is to avoid bounds checking.
// -1 -1 -1 -1
// -1 0 0 -1
// -1 0 0 -1
// -1 -1 -1 -1
field [][]int
2019-06-22 20:20:28 +02:00
// TODO: tetro Tetro
tetro []Block
// TODO: tetros_cache []Tetro
tetros_cache []Block
2019-06-22 20:20:28 +02:00
// Index of the current tetro. Refers to its color.
tetro_idx int
2020-10-02 15:37:00 +02:00
// Idem for the next tetro
next_tetro_idx int
2019-06-22 20:20:28 +02:00
// Index of the rotation (0-3)
rotation_idx int
2019-06-22 20:20:28 +02:00
// gg context for drawing
gg &gg.Context = voidptr(0)
font_loaded bool
show_ghost bool
// frame/time counters:
frame int
frame_old int
frame_sw time.StopWatch = time.new_stopwatch({})
second_sw time.StopWatch = time.new_stopwatch({})
}
[if showfps]
2020-07-24 12:29:47 +02:00
fn (mut game Game) showfps() {
game.frame++
last_frame_ms := f64(game.frame_sw.elapsed().microseconds()) / 1000.0
ticks := f64(game.second_sw.elapsed().microseconds()) / 1000.0
if ticks > 999.0 {
fps := f64(game.frame - game.frame_old) * ticks / 1000.0
2020-09-21 02:42:28 +02:00
$if debug {
eprintln('fps: ${fps:5.1f} | last frame took: ${last_frame_ms:6.3f}ms | frame: ${game.frame:6} ')
}
game.second_sw.restart()
game.frame_old = game.frame
}
2019-06-22 20:20:28 +02:00
}
2020-07-24 12:29:47 +02:00
fn frame(mut game Game) {
game.frame_sw.restart()
game.gg.begin()
game.draw_scene()
game.showfps()
game.gg.end()
}
2019-06-22 20:20:28 +02:00
fn main() {
2020-06-02 15:35:37 +02:00
mut game := &Game{
gg: 0
2020-06-02 15:35:37 +02:00
}
mut fpath := os.resource_abs_path(os.join_path('..', 'assets', 'fonts', 'RobotoMono-Regular.ttf'))
$if android {
fpath = 'fonts/RobotoMono-Regular.ttf'
}
game.gg = gg.new_context(
bg_color: gx.white
width: win_width
height: win_height
use_ortho: true // This is needed for 2D drawing
create_window: true
window_title: 'V Tetris' //
user_data: game
frame_fn: frame
event_fn: on_event
font_path: fpath // wait_events: true
)
2019-06-22 20:20:28 +02:00
game.init_game()
go game.run() // Run the game loop in a new thread
game.gg.run() // Run the render loop in the main thread
2019-06-22 20:20:28 +02:00
}
2020-05-17 13:51:18 +02:00
fn (mut g Game) init_game() {
g.parse_tetros()
g.next_tetro_idx = rand.intn(b_tetros.len) // generate initial "next"
2019-06-22 20:20:28 +02:00
g.generate_tetro()
g.field = []
2019-06-22 20:20:28 +02:00
// Generate the field, fill it with 0's, add -1's on each edge
for _ in 0 .. field_height + 2 {
2020-05-22 17:36:09 +02:00
mut row := [0].repeat(field_width + 2)
row[0] = -1
row[field_width + 1] = -1
2020-11-27 20:41:17 +01:00
g.field << row.clone()
2019-06-22 20:20:28 +02:00
}
for j in 0 .. field_width + 2 {
g.field[0][j] = -1
g.field[field_height + 1][j] = -1
2019-06-22 20:20:28 +02:00
}
2019-08-09 07:28:37 +02:00
g.score = 0
2020-10-02 15:42:05 +02:00
g.lines = 0
g.state = .running
2019-06-22 20:20:28 +02:00
}
2020-05-17 13:51:18 +02:00
fn (mut g Game) parse_tetros() {
for b_tetros0 in b_tetros {
for b_tetro in b_tetros0 {
for t in parse_binary_tetro(b_tetro) {
2019-06-22 20:20:28 +02:00
g.tetros_cache << t
}
}
}
}
2019-06-22 20:20:28 +02:00
2020-05-17 13:51:18 +02:00
fn (mut g Game) run() {
2019-06-22 20:20:28 +02:00
for {
if g.state == .running {
g.move_tetro()
g.delete_completed_lines()
}
// glfw.post_empty_event() // force window redraw
2020-05-22 17:36:09 +02:00
time.sleep_ms(timer_period)
2019-06-22 20:20:28 +02:00
}
}
2020-10-02 09:30:15 +02:00
fn (g &Game) draw_ghost() {
if g.state != .gameover && g.show_ghost {
pos_y := g.move_ghost()
for i in 0 .. tetro_size {
2020-10-02 09:30:15 +02:00
tetro := g.tetro[i]
2020-10-02 15:37:00 +02:00
g.draw_block_color(pos_y + tetro.y, g.pos_x + tetro.x, gx.gray)
2020-10-02 09:30:15 +02:00
}
}
}
fn (g Game) move_ghost() int {
mut pos_y := g.pos_y
mut end := false
for !end {
for block in g.tetro {
y := block.y + pos_y + 1
x := block.x + g.pos_x
if g.field[y][x] != 0 {
end = true
break
}
}
pos_y++
}
return pos_y - 1
}
fn (mut g Game) move_tetro() bool {
2019-06-22 20:20:28 +02:00
// Check each block in current tetro
for block in g.tetro {
y := block.y + g.pos_y + 1
x := block.x + g.pos_x
// Reached the bottom of the screen or another block?
if g.field[y][x] != 0 {
2019-06-22 20:20:28 +02:00
// The new tetro has no space to drop => end of the game
if g.pos_y < 2 {
g.state = .gameover
return false
2019-06-22 20:20:28 +02:00
}
// Drop it and generate a new one
g.drop_tetro()
g.generate_tetro()
return false
2019-06-22 20:20:28 +02:00
}
}
g.pos_y++
return true
2019-06-22 20:20:28 +02:00
}
2020-05-17 13:51:18 +02:00
fn (mut g Game) move_right(dx int) bool {
// Reached left/right edge or another tetro?
for i in 0 .. tetro_size {
2019-06-22 20:20:28 +02:00
tetro := g.tetro[i]
y := tetro.y + g.pos_y
x := tetro.x + g.pos_x + dx
2020-11-27 20:41:17 +01:00
if g.field[y][x] != 0 {
2019-06-22 20:20:28 +02:00
// Do not move
2019-08-09 07:28:37 +02:00
return false
2019-06-22 20:20:28 +02:00
}
}
g.pos_x += dx
2019-08-09 07:28:37 +02:00
return true
2019-06-22 20:20:28 +02:00
}
2020-05-17 13:51:18 +02:00
fn (mut g Game) delete_completed_lines() {
2020-05-22 17:36:09 +02:00
for y := field_height; y >= 1; y-- {
2019-06-22 20:20:28 +02:00
g.delete_completed_line(y)
}
}
2020-05-17 13:51:18 +02:00
fn (mut g Game) delete_completed_line(y int) {
2020-05-22 17:36:09 +02:00
for x := 1; x <= field_width; x++ {
if g.field[y][x] == 0 {
2019-06-22 20:20:28 +02:00
return
}
}
2019-08-09 07:28:37 +02:00
g.score += 10
2020-10-02 15:42:05 +02:00
g.lines++
2019-06-22 20:20:28 +02:00
// Move everything down by 1 position
for yy := y - 1; yy >= 1; yy-- {
2020-05-22 17:36:09 +02:00
for x := 1; x <= field_width; x++ {
g.field[yy + 1][x] = g.field[yy][x]
2019-06-22 20:20:28 +02:00
}
}
}
// Place a new tetro on top
2020-05-17 13:51:18 +02:00
fn (mut g Game) generate_tetro() {
2019-06-22 20:20:28 +02:00
g.pos_y = 0
2020-05-22 17:36:09 +02:00
g.pos_x = field_width / 2 - tetro_size / 2
2020-10-02 15:37:00 +02:00
g.tetro_idx = g.next_tetro_idx
g.next_tetro_idx = rand.intn(b_tetros.len)
g.rotation_idx = 0
g.get_tetro()
2019-06-22 20:20:28 +02:00
}
// Get the right tetro from cache
2020-05-17 13:51:18 +02:00
fn (mut g Game) get_tetro() {
idx := g.tetro_idx * tetro_size * tetro_size + g.rotation_idx * tetro_size
g.tetro = g.tetros_cache[idx..idx + tetro_size]
}
2019-06-22 20:20:28 +02:00
2019-09-17 12:37:25 +02:00
// TODO mut
2020-07-24 12:29:47 +02:00
fn (mut g Game) drop_tetro() {
for i in 0 .. tetro_size {
2019-06-22 20:20:28 +02:00
tetro := g.tetro[i]
x := tetro.x + g.pos_x
y := tetro.y + g.pos_y
// Remember the color of each block
g.field[y][x] = g.tetro_idx + 1
2019-06-22 20:20:28 +02:00
}
}
fn (g &Game) draw_tetro() {
for i in 0 .. tetro_size {
2019-06-22 20:20:28 +02:00
tetro := g.tetro[i]
g.draw_block(g.pos_y + tetro.y, g.pos_x + tetro.x, g.tetro_idx + 1)
}
}
2020-10-02 15:37:00 +02:00
fn (g &Game) draw_next_tetro() {
if g.state != .gameover {
idx := g.next_tetro_idx * tetro_size * tetro_size
next_tetro := g.tetros_cache[idx..idx + tetro_size].clone()
2020-10-02 15:37:00 +02:00
pos_y := 0
pos_x := field_width / 2 - tetro_size / 2
for i in 0 .. tetro_size {
2020-10-02 15:37:00 +02:00
block := next_tetro[i]
g.draw_block_color(pos_y + block.y, pos_x + block.x, gx.rgb(220, 220, 220))
}
}
2020-10-02 09:30:15 +02:00
}
fn (g &Game) draw_block_color(i int, j int, color gx.Color) {
g.gg.draw_rect(f32((j - 1) * block_size), f32((i - 1) * block_size), f32(block_size - 1),
f32(block_size - 1), color)
2019-06-22 20:20:28 +02:00
}
fn (g &Game) draw_block(i int, j int, color_idx int) {
2020-10-02 09:30:15 +02:00
color := if g.state == .gameover { gx.gray } else { colors[color_idx] }
g.draw_block_color(i, j, color)
}
2019-06-22 20:20:28 +02:00
fn (g &Game) draw_field() {
2020-05-22 17:36:09 +02:00
for i := 1; i < field_height + 1; i++ {
for j := 1; j < field_width + 1; j++ {
if g.field[i][j] > 0 {
g.draw_block(i, j, g.field[i][j])
2019-06-22 20:20:28 +02:00
}
}
}
}
2020-05-17 13:51:18 +02:00
fn (mut g Game) draw_ui() {
2020-07-06 19:45:00 +02:00
g.gg.draw_text(1, 3, g.score.str(), text_cfg)
2020-10-02 15:42:05 +02:00
lines := g.lines.str()
g.gg.draw_text(win_width - lines.len * text_size, 3, lines, text_cfg)
2020-07-06 19:45:00 +02:00
if g.state == .gameover {
g.gg.draw_rect(0, win_height / 2 - text_size, win_width, 5 * text_size, ui_color)
2020-07-06 19:45:00 +02:00
g.gg.draw_text(1, win_height / 2 + 0 * text_size, 'Game Over', over_cfg)
g.gg.draw_text(1, win_height / 2 + 2 * text_size, 'Space to restart', over_cfg)
} else if g.state == .paused {
g.gg.draw_rect(0, win_height / 2 - text_size, win_width, 5 * text_size, ui_color)
2020-07-06 19:45:00 +02:00
g.gg.draw_text(1, win_height / 2 + 0 * text_size, 'Game Paused', text_cfg)
g.gg.draw_text(1, win_height / 2 + 2 * text_size, 'SPACE to resume', text_cfg)
2019-08-09 07:28:37 +02:00
}
// g.gg.draw_rect(0, block_size, win_width, limit_thickness, ui_color)
2019-08-09 07:28:37 +02:00
}
2020-05-17 13:51:18 +02:00
fn (mut g Game) draw_scene() {
2020-10-02 09:30:15 +02:00
g.draw_ghost()
2020-10-02 15:37:00 +02:00
g.draw_next_tetro()
2019-06-22 20:20:28 +02:00
g.draw_tetro()
g.draw_field()
g.draw_ui()
2019-06-22 20:20:28 +02:00
}
2019-08-07 14:16:10 +02:00
fn parse_binary_tetro(t_ int) []Block {
2019-08-09 07:28:37 +02:00
mut t := t_
mut res := [Block{}].repeat(4)
2019-06-22 20:20:28 +02:00
mut cnt := 0
horizontal := t == 9 // special case for the horizontal line
ten_powers := [1000, 100, 10, 1]
2019-06-22 20:20:28 +02:00
for i := 0; i <= 3; i++ {
// Get ith digit of t
p := ten_powers[i]
2019-12-07 23:39:27 +01:00
mut digit := t / p
2019-06-22 20:20:28 +02:00
t %= p
// Convert the digit to binary
for j := 3; j >= 0; j-- {
bin := digit % 2
digit /= 2
if bin == 1 || (horizontal && i == tetro_size - 1) {
res[cnt].x = j
res[cnt].y = i
2019-06-22 20:20:28 +02:00
cnt++
}
}
}
return res
}
2020-06-04 10:35:40 +02:00
fn on_event(e &sapp.Event, mut game Game) {
// println('code=$e.char_code')
if e.typ == .key_down {
game.key_down(e.key_code)
2019-06-22 20:20:28 +02:00
}
}
fn (mut game Game) key_down(key sapp.KeyCode) {
// global keys
2019-10-27 08:13:40 +01:00
match key {
.escape {
exit(0)
2019-10-27 08:13:40 +01:00
}
.space {
2019-10-27 08:13:40 +01:00
if game.state == .running {
game.state = .paused
} else if game.state == .paused {
game.state = .running
} else if game.state == .gameover {
game.init_game()
game.state = .running
}
}
else {}
}
if game.state != .running {
return
}
// keys while game is running
2019-10-27 08:13:40 +01:00
match key {
.up {
// Rotate the tetro
old_rotation_idx := game.rotation_idx
game.rotation_idx++
if game.rotation_idx == tetro_size {
game.rotation_idx = 0
}
game.get_tetro()
if !game.move_right(0) {
game.rotation_idx = old_rotation_idx
game.get_tetro()
}
if game.pos_x < 0 {
// game.pos_x = 1
}
2019-08-09 07:28:37 +02:00
}
.left {
game.move_right(-1)
2019-06-22 20:20:28 +02:00
}
.right {
game.move_right(1)
}
.down {
game.move_tetro() // drop faster when the player presses <down>
}
.d {
for game.move_tetro() {
}
}
2020-10-02 09:30:15 +02:00
.g {
game.show_ghost = !game.show_ghost
}
else {}
2019-10-27 08:13:40 +01:00
}
2019-06-22 20:20:28 +02:00
}