519 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			V
		
	
	
			
		
		
	
	
			519 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			V
		
	
	
| // Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved.
 | |
| // Use of this source code is governed by an MIT license
 | |
| // that can be found in the LICENSE file.
 | |
| module main
 | |
| 
 | |
| import os
 | |
| import rand
 | |
| import time
 | |
| import gx
 | |
| import gg
 | |
| // import sokol.sapp
 | |
| 
 | |
| const (
 | |
| 	block_size      = 20 // virtual 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
 | |
| 	limit_thickness = 3
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	text_cfg = gx.TextCfg{
 | |
| 		align: .left
 | |
| 		size: text_size
 | |
| 		color: gx.rgb(0, 0, 0)
 | |
| 	}
 | |
| 	over_cfg = gx.TextCfg{
 | |
| 		align: .left
 | |
| 		size: text_size
 | |
| 		color: gx.white
 | |
| 	}
 | |
| )
 | |
| 
 | |
| 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         = [
 | |
| 		[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),
 | |
| 	]
 | |
| 	background_color = gx.white
 | |
| 	ui_color         = gx.rgba(255, 0, 0, 210)
 | |
| )
 | |
| 
 | |
| // TODO: type Tetro [tetro_size]struct{ x, y int }
 | |
| struct Block {
 | |
| mut:
 | |
| 	x int
 | |
| 	y int
 | |
| }
 | |
| 
 | |
| enum GameState {
 | |
| 	paused
 | |
| 	running
 | |
| 	gameover
 | |
| }
 | |
| 
 | |
| struct Game {
 | |
| mut:
 | |
| 	// Score of the current game
 | |
| 	score int
 | |
| 	// Lines of the current game
 | |
| 	lines int
 | |
| 	// State of the current game
 | |
| 	state GameState
 | |
| 	// Block size in screen dimensions
 | |
| 	block_size int = block_size
 | |
| 	// Field margin
 | |
| 	margin int
 | |
| 	// Position of the current tetro
 | |
| 	pos_x int
 | |
| 	pos_y int
 | |
| 	// 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
 | |
| 	// TODO: tetro Tetro
 | |
| 	tetro []Block
 | |
| 	// TODO: tetros_cache []Tetro
 | |
| 	tetros_cache []Block
 | |
| 	// Index of the current tetro. Refers to its color.
 | |
| 	tetro_idx int
 | |
| 	// Idem for the next tetro
 | |
| 	next_tetro_idx int
 | |
| 	// Index of the rotation (0-3)
 | |
| 	rotation_idx int
 | |
| 	// gg context for drawing
 | |
| 	gg          &gg.Context = voidptr(0)
 | |
| 	font_loaded bool
 | |
| 	show_ghost  bool = true
 | |
| 	// frame/time counters:
 | |
| 	frame     int
 | |
| 	frame_old int
 | |
| 	frame_sw  time.StopWatch = time.new_stopwatch({})
 | |
| 	second_sw time.StopWatch = time.new_stopwatch({})
 | |
| }
 | |
| 
 | |
| fn remap(v f32, min f32, max f32, new_min f32, new_max f32) f32 {
 | |
| 	return (((v - min) * (new_max - new_min)) / (max - min)) + new_min
 | |
| }
 | |
| 
 | |
| [if showfps ?]
 | |
| 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
 | |
| 		$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
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn frame(mut game Game) {
 | |
| 	ws := gg.window_size()
 | |
| 	bs := remap(block_size, 0, win_height, 0, ws.height)
 | |
| 	m := (f32(ws.width) - bs * field_width) * 0.5
 | |
| 	game.block_size = int(bs)
 | |
| 	game.margin = int(m)
 | |
| 	game.frame_sw.restart()
 | |
| 	game.gg.begin()
 | |
| 	game.draw_scene()
 | |
| 	game.showfps()
 | |
| 	game.gg.end()
 | |
| }
 | |
| 
 | |
| fn main() {
 | |
| 	mut game := &Game{
 | |
| 		gg: 0
 | |
| 	}
 | |
| 	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
 | |
| 		create_window: true
 | |
| 		window_title: 'V Tetris' //
 | |
| 		user_data: game
 | |
| 		frame_fn: frame
 | |
| 		event_fn: on_event
 | |
| 		font_path: fpath // wait_events: true
 | |
| 	)
 | |
| 	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
 | |
| }
 | |
| 
 | |
| fn (mut g Game) init_game() {
 | |
| 	g.parse_tetros()
 | |
| 	g.next_tetro_idx = rand.intn(b_tetros.len) // generate initial "next"
 | |
| 	g.generate_tetro()
 | |
| 	g.field = []
 | |
| 	// Generate the field, fill it with 0's, add -1's on each edge
 | |
| 	for _ in 0 .. field_height + 2 {
 | |
| 		mut row := [0].repeat(field_width + 2)
 | |
| 		row[0] = -1
 | |
| 		row[field_width + 1] = -1
 | |
| 		g.field << row.clone()
 | |
| 	}
 | |
| 	for j in 0 .. field_width + 2 {
 | |
| 		g.field[0][j] = -1
 | |
| 		g.field[field_height + 1][j] = -1
 | |
| 	}
 | |
| 	g.score = 0
 | |
| 	g.lines = 0
 | |
| 	g.state = .running
 | |
| }
 | |
| 
 | |
| 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) {
 | |
| 				g.tetros_cache << t
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (mut g Game) run() {
 | |
| 	for {
 | |
| 		if g.state == .running {
 | |
| 			g.move_tetro()
 | |
| 			g.delete_completed_lines()
 | |
| 		}
 | |
| 		// glfw.post_empty_event() // force window redraw
 | |
| 		time.sleep(timer_period * time.millisecond)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (g &Game) draw_ghost() {
 | |
| 	if g.state != .gameover && g.show_ghost {
 | |
| 		pos_y := g.move_ghost()
 | |
| 		for i in 0 .. tetro_size {
 | |
| 			tetro := g.tetro[i]
 | |
| 			g.draw_block_color(pos_y + tetro.y, g.pos_x + tetro.x, gx.rgba(125, 125, 225,
 | |
| 				40))
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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 {
 | |
| 	// 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 {
 | |
| 			// The new tetro has no space to drop => end of the game
 | |
| 			if g.pos_y < 2 {
 | |
| 				g.state = .gameover
 | |
| 				return false
 | |
| 			}
 | |
| 			// Drop it and generate a new one
 | |
| 			g.drop_tetro()
 | |
| 			g.generate_tetro()
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	g.pos_y++
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| fn (mut g Game) move_right(dx int) bool {
 | |
| 	// Reached left/right edge or another tetro?
 | |
| 	for i in 0 .. tetro_size {
 | |
| 		tetro := g.tetro[i]
 | |
| 		y := tetro.y + g.pos_y
 | |
| 		x := tetro.x + g.pos_x + dx
 | |
| 		if g.field[y][x] != 0 {
 | |
| 			// Do not move
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	g.pos_x += dx
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| fn (mut g Game) delete_completed_lines() {
 | |
| 	for y := field_height; y >= 1; y-- {
 | |
| 		g.delete_completed_line(y)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (mut g Game) delete_completed_line(y int) {
 | |
| 	for x := 1; x <= field_width; x++ {
 | |
| 		if g.field[y][x] == 0 {
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	g.score += 10
 | |
| 	g.lines++
 | |
| 	// Move everything down by 1 position
 | |
| 	for yy := y - 1; yy >= 1; yy-- {
 | |
| 		for x := 1; x <= field_width; x++ {
 | |
| 			g.field[yy + 1][x] = g.field[yy][x]
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Place a new tetro on top
 | |
| fn (mut g Game) generate_tetro() {
 | |
| 	g.pos_y = 0
 | |
| 	g.pos_x = field_width / 2 - tetro_size / 2
 | |
| 	g.tetro_idx = g.next_tetro_idx
 | |
| 	g.next_tetro_idx = rand.intn(b_tetros.len)
 | |
| 	g.rotation_idx = 0
 | |
| 	g.get_tetro()
 | |
| }
 | |
| 
 | |
| // Get the right tetro from cache
 | |
| 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].clone()
 | |
| }
 | |
| 
 | |
| // TODO mut
 | |
| fn (mut g Game) drop_tetro() {
 | |
| 	for i in 0 .. tetro_size {
 | |
| 		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
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (g &Game) draw_tetro() {
 | |
| 	for i in 0 .. tetro_size {
 | |
| 		tetro := g.tetro[i]
 | |
| 		g.draw_block(g.pos_y + tetro.y, g.pos_x + tetro.x, g.tetro_idx + 1)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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()
 | |
| 		pos_y := 0
 | |
| 		pos_x := field_width / 2 - tetro_size / 2
 | |
| 		for i in 0 .. tetro_size {
 | |
| 			block := next_tetro[i]
 | |
| 			g.draw_block_color(pos_y + block.y, pos_x + block.x, gx.rgb(220, 220, 220))
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (g &Game) draw_block_color(i int, j int, color gx.Color) {
 | |
| 	g.gg.draw_rect(f32((j - 1) * g.block_size) + g.margin, f32((i - 1) * g.block_size),
 | |
| 		f32(g.block_size - 1), f32(g.block_size - 1), color)
 | |
| }
 | |
| 
 | |
| fn (g &Game) draw_block(i int, j int, color_idx int) {
 | |
| 	color := if g.state == .gameover { gx.gray } else { colors[color_idx] }
 | |
| 	g.draw_block_color(i, j, color)
 | |
| }
 | |
| 
 | |
| fn (g &Game) draw_field() {
 | |
| 	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])
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (mut g Game) draw_ui() {
 | |
| 	ws := gg.window_size()
 | |
| 	textsize := int(remap(text_size, 0, win_width, 0, ws.width))
 | |
| 	g.gg.draw_text(1, 3, g.score.str(), text_cfg)
 | |
| 	lines := g.lines.str()
 | |
| 	g.gg.draw_text(ws.width - lines.len * textsize, 3, lines, text_cfg)
 | |
| 	if g.state == .gameover {
 | |
| 		g.gg.draw_rect(0, ws.height / 2 - textsize, ws.width, 5 * textsize, ui_color)
 | |
| 		g.gg.draw_text(1, ws.height / 2 + 0 * textsize, 'Game Over', over_cfg)
 | |
| 		g.gg.draw_text(1, ws.height / 2 + 2 * textsize, 'Space to restart', over_cfg)
 | |
| 	} else if g.state == .paused {
 | |
| 		g.gg.draw_rect(0, ws.height / 2 - textsize, ws.width, 5 * textsize, ui_color)
 | |
| 		g.gg.draw_text(1, ws.height / 2 + 0 * textsize, 'Game Paused', text_cfg)
 | |
| 		g.gg.draw_text(1, ws.height / 2 + 2 * textsize, 'SPACE to resume', text_cfg)
 | |
| 	}
 | |
| 	// g.gg.draw_rect(0, block_size, win_width, limit_thickness, ui_color)
 | |
| }
 | |
| 
 | |
| fn (mut g Game) draw_scene() {
 | |
| 	g.draw_ghost()
 | |
| 	g.draw_next_tetro()
 | |
| 	g.draw_tetro()
 | |
| 	g.draw_field()
 | |
| 	g.draw_ui()
 | |
| }
 | |
| 
 | |
| fn parse_binary_tetro(t_ int) []Block {
 | |
| 	mut t := t_
 | |
| 	mut res := [Block{}].repeat(4)
 | |
| 	mut cnt := 0
 | |
| 	horizontal := t == 9 // special case for the horizontal line
 | |
| 	ten_powers := [1000, 100, 10, 1]
 | |
| 	for i := 0; i <= 3; i++ {
 | |
| 		// Get ith digit of t
 | |
| 		p := ten_powers[i]
 | |
| 		mut digit := t / p
 | |
| 		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
 | |
| 				cnt++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| fn on_event(e &gg.Event, mut game Game) {
 | |
| 	// println('code=$e.char_code')
 | |
| 	if e.typ == .key_down {
 | |
| 		game.key_down(e.key_code)
 | |
| 	}
 | |
| 	if e.typ == .touches_began || e.typ == .touches_moved {
 | |
| 		if e.num_touches > 0 {
 | |
| 			touch_point := e.touches[0]
 | |
| 			game.touch_event(touch_point)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (mut game Game) rotate_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
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (mut game Game) key_down(key gg.KeyCode) {
 | |
| 	// global keys
 | |
| 	match key {
 | |
| 		.escape {
 | |
| 			exit(0)
 | |
| 		}
 | |
| 		.space {
 | |
| 			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
 | |
| 	match key {
 | |
| 		.up {
 | |
| 			// Rotate the tetro
 | |
| 			game.rotate_tetro()
 | |
| 		}
 | |
| 		.left {
 | |
| 			game.move_right(-1)
 | |
| 		}
 | |
| 		.right {
 | |
| 			game.move_right(1)
 | |
| 		}
 | |
| 		.down {
 | |
| 			game.move_tetro() // drop faster when the player presses <down>
 | |
| 		}
 | |
| 		.d {
 | |
| 			for game.move_tetro() {
 | |
| 			}
 | |
| 		}
 | |
| 		.g {
 | |
| 			game.show_ghost = !game.show_ghost
 | |
| 		}
 | |
| 		else {}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (mut game Game) touch_event(touch_point C.sapp_touchpoint) {
 | |
| 	ws := gg.window_size()
 | |
| 	tx := touch_point.pos_x
 | |
| 	ty := touch_point.pos_y
 | |
| 	if ty < f32(ws.height) * 0.5 {
 | |
| 		game.rotate_tetro()
 | |
| 	} else {
 | |
| 		if tx <= f32(ws.width) * 0.5 {
 | |
| 			game.move_right(-1)
 | |
| 		} else {
 | |
| 			game.move_right(1)
 | |
| 		}
 | |
| 	}
 | |
| }
 |