476 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			V
		
	
	
			
		
		
	
	
			476 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			V
		
	
	
// import modules for use in app
 | 
						|
import term.ui as termui
 | 
						|
import rand
 | 
						|
 | 
						|
// define some global constants
 | 
						|
const (
 | 
						|
	block_size = 1
 | 
						|
	buffer     = 10
 | 
						|
	green      = termui.Color{0, 255, 0}
 | 
						|
	grey       = termui.Color{150, 150, 150}
 | 
						|
	white      = termui.Color{255, 255, 255}
 | 
						|
	blue       = termui.Color{0, 0, 255}
 | 
						|
	red        = termui.Color{255, 0, 0}
 | 
						|
	black      = termui.Color{0, 0, 0}
 | 
						|
)
 | 
						|
 | 
						|
// what edge of the screen are you facing
 | 
						|
enum Orientation {
 | 
						|
	top
 | 
						|
	right
 | 
						|
	bottom
 | 
						|
	left
 | 
						|
}
 | 
						|
 | 
						|
// what's the current state of the game
 | 
						|
enum GameState {
 | 
						|
	pause
 | 
						|
	gameover
 | 
						|
	game
 | 
						|
	oob // snake out-of-bounds
 | 
						|
}
 | 
						|
 | 
						|
// simple 2d vector representation
 | 
						|
struct Vec {
 | 
						|
mut:
 | 
						|
	x int
 | 
						|
	y int
 | 
						|
}
 | 
						|
 | 
						|
// determine orientation from vector (hacky way to set facing from velocity)
 | 
						|
fn (v Vec) facing() Orientation {
 | 
						|
	result := if v.x >= 0 {
 | 
						|
		Orientation.right
 | 
						|
	} else if v.x < 0 {
 | 
						|
		Orientation.left
 | 
						|
	} else if v.y >= 0 {
 | 
						|
		Orientation.bottom
 | 
						|
	} else {
 | 
						|
		Orientation.top
 | 
						|
	}
 | 
						|
	return result
 | 
						|
}
 | 
						|
 | 
						|
// generate a random vector with x in [min_x, max_x] and y in [min_y, max_y]
 | 
						|
fn (mut v Vec) randomize(min_x int, min_y int, max_x int, max_y int) {
 | 
						|
	v.x = rand.int_in_range(min_x, max_x)
 | 
						|
	v.y = rand.int_in_range(min_y, max_y)
 | 
						|
}
 | 
						|
 | 
						|
// part of snake's body representation
 | 
						|
struct BodyPart {
 | 
						|
mut:
 | 
						|
	pos Vec = Vec{
 | 
						|
		x: block_size
 | 
						|
		y: block_size
 | 
						|
	}
 | 
						|
	color  termui.Color = green
 | 
						|
	facing Orientation  = .top
 | 
						|
}
 | 
						|
 | 
						|
// snake representation
 | 
						|
struct Snake {
 | 
						|
mut:
 | 
						|
	app       &App
 | 
						|
	direction Orientation
 | 
						|
	body      []BodyPart
 | 
						|
	velocity  Vec = Vec{
 | 
						|
		x: 0
 | 
						|
		y: 0
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// length returns the snake's current length
 | 
						|
fn (s Snake) length() int {
 | 
						|
	return s.body.len
 | 
						|
}
 | 
						|
 | 
						|
// impulse provides a impulse to change the snake's direction
 | 
						|
fn (mut s Snake) impulse(direction Orientation) {
 | 
						|
	mut vec := Vec{}
 | 
						|
	match direction {
 | 
						|
		.top {
 | 
						|
			vec.x = 0
 | 
						|
			vec.y = -1 * block_size
 | 
						|
		}
 | 
						|
		.right {
 | 
						|
			vec.x = 2 * block_size
 | 
						|
			vec.y = 0
 | 
						|
		}
 | 
						|
		.bottom {
 | 
						|
			vec.x = 0
 | 
						|
			vec.y = block_size
 | 
						|
		}
 | 
						|
		.left {
 | 
						|
			vec.x = -2 * block_size
 | 
						|
			vec.y = 0
 | 
						|
		}
 | 
						|
	}
 | 
						|
	s.direction = direction
 | 
						|
	s.velocity = vec
 | 
						|
}
 | 
						|
 | 
						|
// move performs the calculations for the snake's movements
 | 
						|
fn (mut s Snake) move() {
 | 
						|
	mut i := s.body.len - 1
 | 
						|
	width := s.app.width
 | 
						|
	height := s.app.height
 | 
						|
	// move the parts of the snake as appropriate
 | 
						|
	for i = s.body.len - 1; i >= 0; i-- {
 | 
						|
		mut piece := s.body[i]
 | 
						|
		if i > 0 { // just move the body of the snake up one position
 | 
						|
			piece.pos = s.body[i - 1].pos
 | 
						|
			piece.facing = s.body[i - 1].facing
 | 
						|
		} else { // verify that the move is valid and move the head if so
 | 
						|
			piece.facing = s.direction
 | 
						|
			new_x := piece.pos.x + s.velocity.x
 | 
						|
			new_y := piece.pos.y + s.velocity.y
 | 
						|
			piece.pos.x += if new_x > block_size && new_x < width - block_size {
 | 
						|
				s.velocity.x
 | 
						|
			} else {
 | 
						|
				0
 | 
						|
			}
 | 
						|
			piece.pos.y += if new_y > block_size && new_y < height - block_size {
 | 
						|
				s.velocity.y
 | 
						|
			} else {
 | 
						|
				0
 | 
						|
			}
 | 
						|
		}
 | 
						|
		s.body[i] = piece
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// grow add another part to the snake when it catches the rat
 | 
						|
fn (mut s Snake) grow() {
 | 
						|
	head := s.get_tail()
 | 
						|
	mut pos := Vec{}
 | 
						|
	// add the segment on the opposite side of the previous tail
 | 
						|
	match head.facing {
 | 
						|
		.bottom {
 | 
						|
			pos.x = head.pos.x
 | 
						|
			pos.y = head.pos.y - block_size
 | 
						|
		}
 | 
						|
		.left {
 | 
						|
			pos.x = head.pos.x + block_size
 | 
						|
			pos.y = head.pos.y
 | 
						|
		}
 | 
						|
		.top {
 | 
						|
			pos.x = head.pos.x
 | 
						|
			pos.y = head.pos.y + block_size
 | 
						|
		}
 | 
						|
		.right {
 | 
						|
			pos.x = head.pos.x - block_size
 | 
						|
			pos.y = head.pos.y
 | 
						|
		}
 | 
						|
	}
 | 
						|
	s.body << BodyPart{
 | 
						|
		pos: pos
 | 
						|
		facing: head.facing
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// get_body gets the parts of the snakes body
 | 
						|
fn (s Snake) get_body() []BodyPart {
 | 
						|
	return s.body
 | 
						|
}
 | 
						|
 | 
						|
// get_head get snake's head
 | 
						|
fn (s Snake) get_head() BodyPart {
 | 
						|
	return s.body[0]
 | 
						|
}
 | 
						|
 | 
						|
// get_tail get snake's tail
 | 
						|
fn (s Snake) get_tail() BodyPart {
 | 
						|
	return s.body[s.body.len - 1]
 | 
						|
}
 | 
						|
 | 
						|
// randomize randomizes position and veolcity of snake
 | 
						|
fn (mut s Snake) randomize() {
 | 
						|
	speeds := [-2, 0, 2]
 | 
						|
	mut pos := s.get_head().pos
 | 
						|
	pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer)
 | 
						|
	for pos.x % 2 != 0 || (pos.x < buffer && pos.x > s.app.width - buffer) {
 | 
						|
		pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer)
 | 
						|
	}
 | 
						|
	s.velocity.y = rand.int_in_range(-1 * block_size, block_size)
 | 
						|
	s.velocity.x = speeds[rand.intn(speeds.len)]
 | 
						|
	s.direction = s.velocity.facing()
 | 
						|
	s.body[0].pos = pos
 | 
						|
}
 | 
						|
 | 
						|
// check_overlap determine if the snake's looped onto itself
 | 
						|
fn (s Snake) check_overlap() bool {
 | 
						|
	h := s.get_head()
 | 
						|
	head_pos := h.pos
 | 
						|
	for i in 2 .. s.length() {
 | 
						|
		piece_pos := s.body[i].pos
 | 
						|
		if head_pos.x == piece_pos.x && head_pos.y == piece_pos.y {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
fn (s Snake) check_out_of_bounds() bool {
 | 
						|
	h := s.get_head()
 | 
						|
	return h.pos.x + s.velocity.x <= block_size
 | 
						|
		|| h.pos.x + s.velocity.x > s.app.width - s.velocity.x
 | 
						|
		|| h.pos.y + s.velocity.y <= block_size
 | 
						|
		|| h.pos.y + s.velocity.y > s.app.height - block_size - s.velocity.y
 | 
						|
}
 | 
						|
 | 
						|
// draw draws the parts of the snake
 | 
						|
fn (s Snake) draw() {
 | 
						|
	mut a := s.app
 | 
						|
	for part in s.get_body() {
 | 
						|
		a.termui.set_bg_color(part.color)
 | 
						|
		a.termui.draw_rect(part.pos.x, part.pos.y, part.pos.x + block_size, part.pos.y + block_size)
 | 
						|
		$if verbose ? {
 | 
						|
			text := match part.facing {
 | 
						|
				.top { '^' }
 | 
						|
				.bottom { 'v' }
 | 
						|
				.right { '>' }
 | 
						|
				.left { '<' }
 | 
						|
			}
 | 
						|
			a.termui.set_color(white)
 | 
						|
			a.termui.draw_text(part.pos.x, part.pos.y, text)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// rat representation
 | 
						|
struct Rat {
 | 
						|
mut:
 | 
						|
	pos Vec = Vec{
 | 
						|
		x: block_size
 | 
						|
		y: block_size
 | 
						|
	}
 | 
						|
	captured bool
 | 
						|
	color    termui.Color = grey
 | 
						|
	app      &App
 | 
						|
}
 | 
						|
 | 
						|
// randomize spawn the rat in a new spot within the playable field
 | 
						|
fn (mut r Rat) randomize() {
 | 
						|
	r.pos.randomize(2 * block_size + buffer, 2 * block_size + buffer, r.app.width - block_size - buffer,
 | 
						|
		r.app.height - block_size - buffer)
 | 
						|
}
 | 
						|
 | 
						|
[heap]
 | 
						|
struct App {
 | 
						|
mut:
 | 
						|
	termui &termui.Context = 0
 | 
						|
	snake  Snake
 | 
						|
	rat    Rat
 | 
						|
	width  int
 | 
						|
	height int
 | 
						|
	redraw bool      = true
 | 
						|
	state  GameState = .game
 | 
						|
}
 | 
						|
 | 
						|
// new_game setups the rat and snake for play
 | 
						|
fn (mut a App) new_game() {
 | 
						|
	mut snake := Snake{
 | 
						|
		body: []BodyPart{len: 1, init: BodyPart{}}
 | 
						|
		app: a
 | 
						|
	}
 | 
						|
	snake.randomize()
 | 
						|
	mut rat := Rat{
 | 
						|
		app: a
 | 
						|
	}
 | 
						|
	rat.randomize()
 | 
						|
	a.snake = snake
 | 
						|
	a.rat = rat
 | 
						|
	a.state = .game
 | 
						|
	a.redraw = true
 | 
						|
}
 | 
						|
 | 
						|
// initialize the app and record the width and height of the window
 | 
						|
fn init(x voidptr) {
 | 
						|
	mut app := &App(x)
 | 
						|
	w, h := app.termui.window_width, app.termui.window_height
 | 
						|
	app.width = w
 | 
						|
	app.height = h
 | 
						|
	app.new_game()
 | 
						|
}
 | 
						|
 | 
						|
// event handles different events for the app as they occur
 | 
						|
fn event(e &termui.Event, x voidptr) {
 | 
						|
	mut app := &App(x)
 | 
						|
	match e.typ {
 | 
						|
		.mouse_down {}
 | 
						|
		.mouse_drag {}
 | 
						|
		.mouse_up {}
 | 
						|
		.key_down {
 | 
						|
			match e.code {
 | 
						|
				.up, .w { app.move_snake(.top) }
 | 
						|
				.down, .s { app.move_snake(.bottom) }
 | 
						|
				.left, .a { app.move_snake(.left) }
 | 
						|
				.right, .d { app.move_snake(.right) }
 | 
						|
				.r { app.new_game() }
 | 
						|
				.c {}
 | 
						|
				.p { app.state = if app.state == .game { GameState.pause } else { GameState.game } }
 | 
						|
				.escape, .q { exit(0) }
 | 
						|
				else { exit(0) }
 | 
						|
			}
 | 
						|
			if e.code == .c {
 | 
						|
			} else if e.code == .escape {
 | 
						|
				exit(0)
 | 
						|
			}
 | 
						|
		}
 | 
						|
		else {}
 | 
						|
	}
 | 
						|
	app.redraw = true
 | 
						|
}
 | 
						|
 | 
						|
// frame perform actions on every tick
 | 
						|
fn frame(x voidptr) {
 | 
						|
	mut app := &App(x)
 | 
						|
	app.update()
 | 
						|
	app.draw()
 | 
						|
}
 | 
						|
 | 
						|
// update perform any calculations that are needed before drawing
 | 
						|
fn (mut a App) update() {
 | 
						|
	if a.state == .game {
 | 
						|
		a.snake.move()
 | 
						|
		if a.snake.check_out_of_bounds() {
 | 
						|
			$if verbose ? {
 | 
						|
				a.snake.body[0].color = red
 | 
						|
			} $else {
 | 
						|
				a.state = .oob
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if a.snake.check_overlap() {
 | 
						|
			a.state = .gameover
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if a.check_capture() {
 | 
						|
			a.rat.randomize()
 | 
						|
			a.snake.grow()
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// draw write to the screen
 | 
						|
fn (mut a App) draw() {
 | 
						|
	// reset screen
 | 
						|
	a.termui.clear()
 | 
						|
	a.termui.set_bg_color(white)
 | 
						|
	a.termui.draw_empty_rect(1, 1, a.width, a.height)
 | 
						|
	// determine if a special screen needs to be draw
 | 
						|
	match a.state {
 | 
						|
		.gameover {
 | 
						|
			a.draw_gameover()
 | 
						|
			a.redraw = false
 | 
						|
		}
 | 
						|
		.pause {
 | 
						|
			a.draw_pause()
 | 
						|
		}
 | 
						|
		else {
 | 
						|
			a.redraw = true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	a.termui.set_color(blue)
 | 
						|
	a.termui.set_bg_color(white)
 | 
						|
	a.termui.draw_text(3 * block_size, a.height - (2 * block_size), 'p - (un)pause r - reset q - quit')
 | 
						|
	// draw the snake, rat, and score if appropriate
 | 
						|
	if a.redraw {
 | 
						|
		a.termui.set_bg_color(black)
 | 
						|
		a.draw_gamescreen()
 | 
						|
		if a.state == .oob {
 | 
						|
			a.state = .gameover
 | 
						|
		}
 | 
						|
	}
 | 
						|
	// write to the screen
 | 
						|
	a.termui.reset_bg_color()
 | 
						|
	a.termui.flush()
 | 
						|
}
 | 
						|
 | 
						|
// move_snake move the snake in specified direction
 | 
						|
fn (mut a App) move_snake(direction Orientation) {
 | 
						|
	a.snake.impulse(direction)
 | 
						|
}
 | 
						|
 | 
						|
// check_capture determine if the snake overlaps with the rat
 | 
						|
fn (a App) check_capture() bool {
 | 
						|
	snake_pos := a.snake.get_head().pos
 | 
						|
	rat_pos := a.rat.pos
 | 
						|
	return snake_pos.x <= rat_pos.x + block_size && snake_pos.x + block_size >= rat_pos.x
 | 
						|
		&& snake_pos.y <= rat_pos.y + block_size && snake_pos.y + block_size >= rat_pos.y
 | 
						|
}
 | 
						|
 | 
						|
fn (mut a App) draw_snake() {
 | 
						|
	a.snake.draw()
 | 
						|
}
 | 
						|
 | 
						|
fn (mut a App) draw_rat() {
 | 
						|
	a.termui.set_bg_color(a.rat.color)
 | 
						|
	a.termui.draw_rect(a.rat.pos.x, a.rat.pos.y, a.rat.pos.x + block_size, a.rat.pos.y + block_size)
 | 
						|
}
 | 
						|
 | 
						|
fn (mut a App) draw_gamescreen() {
 | 
						|
	$if verbose ? {
 | 
						|
		a.draw_debug()
 | 
						|
	}
 | 
						|
	a.draw_score()
 | 
						|
	a.draw_rat()
 | 
						|
	a.draw_snake()
 | 
						|
}
 | 
						|
 | 
						|
fn (mut a App) draw_score() {
 | 
						|
	a.termui.set_color(blue)
 | 
						|
	a.termui.set_bg_color(white)
 | 
						|
	score := a.snake.length() - 1
 | 
						|
	a.termui.draw_text(a.width - (2 * block_size), block_size, '${score:03d}')
 | 
						|
}
 | 
						|
 | 
						|
fn (mut a App) draw_pause() {
 | 
						|
	a.termui.set_color(blue)
 | 
						|
	a.termui.draw_text((a.width / 2) - block_size, 3 * block_size, 'Paused!')
 | 
						|
}
 | 
						|
 | 
						|
fn (mut a App) draw_debug() {
 | 
						|
	a.termui.set_color(blue)
 | 
						|
	a.termui.set_bg_color(white)
 | 
						|
	snake := a.snake
 | 
						|
	a.termui.draw_text(block_size, 1 * block_size, 'Display_width: ${a.width:04d} Display_height: ${a.height:04d}')
 | 
						|
	a.termui.draw_text(block_size, 2 * block_size, 'Vx: ${snake.velocity.x:+02d} Vy: ${snake.velocity.y:+02d}')
 | 
						|
	a.termui.draw_text(block_size, 3 * block_size, 'F: $snake.direction')
 | 
						|
	snake_head := snake.get_head()
 | 
						|
	rat := a.rat
 | 
						|
	a.termui.draw_text(block_size, 4 * block_size, 'Sx: ${snake_head.pos.x:+03d} Sy: ${snake_head.pos.y:+03d}')
 | 
						|
	a.termui.draw_text(block_size, 5 * block_size, 'Rx: ${rat.pos.x:+03d} Ry: ${rat.pos.y:+03d}')
 | 
						|
}
 | 
						|
 | 
						|
fn (mut a App) draw_gameover() {
 | 
						|
	a.termui.set_bg_color(white)
 | 
						|
	a.termui.set_color(red)
 | 
						|
	a.rat.pos = Vec{
 | 
						|
		x: -1
 | 
						|
		y: -1
 | 
						|
	}
 | 
						|
	x_offset := '   #####                        '.len // take half of a line from the game over text and store the length
 | 
						|
	start_x := (a.width / 2) - x_offset
 | 
						|
	a.termui.draw_text(start_x, (a.height / 2) - 3 * block_size, '   #####                         #######                       ')
 | 
						|
	a.termui.draw_text(start_x, (a.height / 2) - 2 * block_size, '  #     #   ##   #    # ######   #     # #    # ###### #####   ')
 | 
						|
	a.termui.draw_text(start_x, (a.height / 2) - 1 * block_size, '  #        #  #  ##  ## #        #     # #    # #      #    #  ')
 | 
						|
	a.termui.draw_text(start_x, (a.height / 2) - 0 * block_size, '  #  #### #    # # ## # #####    #     # #    # #####  #    #  ')
 | 
						|
	a.termui.draw_text(start_x, (a.height / 2) + 1 * block_size, '  #     # ###### #    # #        #     # #    # #      #####   ')
 | 
						|
	a.termui.draw_text(start_x, (a.height / 2) + 2 * block_size, '  #     # #    # #    # #        #     #  #  #  #      #   #   ')
 | 
						|
	a.termui.draw_text(start_x, (a.height / 2) + 3 * block_size, '   #####  #    # #    # ######   #######   ##   ###### #    #  ')
 | 
						|
}
 | 
						|
 | 
						|
fn main() {
 | 
						|
	mut app := &App{}
 | 
						|
	app.termui = termui.init(
 | 
						|
		user_data: app
 | 
						|
		event_fn: event
 | 
						|
		frame_fn: frame
 | 
						|
		init_fn: init
 | 
						|
		hide_cursor: true
 | 
						|
		frame_rate: 10
 | 
						|
	)
 | 
						|
	app.termui.run() ?
 | 
						|
}
 |