| 1 | import gg |
| 2 | import os |
| 3 | import rand |
| 4 | import time |
| 5 | import math.vec { Vec2 } |
| 6 | |
| 7 | // constants |
| 8 | const font = $embed_file('../assets/fonts/RobotoMono-Regular.ttf') |
| 9 | const top_height = 100 |
| 10 | const canvas_size = 700 |
| 11 | const game_size = 17 |
| 12 | const tile_size = canvas_size / game_size |
| 13 | const tick_rate_ms = 100 |
| 14 | const high_score_file_path = os.join_path(os.cache_dir(), 'v', 'examples', 'snek') |
| 15 | |
| 16 | // types |
| 17 | type HighScore = int |
| 18 | type Vec = Vec2[int] |
| 19 | |
| 20 | fn (mut h HighScore) save() { |
| 21 | os.mkdir_all(os.dir(high_score_file_path)) or { return } |
| 22 | os.write_file(high_score_file_path, (*h).str()) or { return } |
| 23 | } |
| 24 | |
| 25 | fn (mut h HighScore) load() { |
| 26 | h = (os.read_file(high_score_file_path) or { '' }).int() |
| 27 | } |
| 28 | |
| 29 | struct App { |
| 30 | mut: |
| 31 | gg &gg.Context = unsafe { nil } |
| 32 | score int |
| 33 | best HighScore |
| 34 | snake []Vec |
| 35 | dir Vec |
| 36 | dir_queue []Vec |
| 37 | food Vec |
| 38 | last_tick i64 |
| 39 | } |
| 40 | |
| 41 | // utility |
| 42 | fn (mut app App) reset_game() { |
| 43 | app.score = 0 |
| 44 | app.snake = [Vec{3, 8}, Vec{2, 8}, Vec{1, 8}, Vec{0, 8}] |
| 45 | app.dir = Vec{1, 0} |
| 46 | app.dir_queue = [] |
| 47 | app.food = Vec{10, 8} |
| 48 | app.last_tick = time.ticks() |
| 49 | } |
| 50 | |
| 51 | fn (mut app App) move_food() { |
| 52 | for { |
| 53 | x := rand.intn(game_size) or { 0 } |
| 54 | y := rand.intn(game_size) or { 0 } |
| 55 | app.food = Vec{x, y} |
| 56 | if app.food !in app.snake { |
| 57 | return |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | fn on_frame(mut app App) { |
| 63 | // check if snake bit itself |
| 64 | if app.snake[0] in app.snake[1..] { |
| 65 | app.reset_game() |
| 66 | } |
| 67 | |
| 68 | // check if snake hit a wall |
| 69 | if app.snake[0].x < 0 || app.snake[0].x >= game_size || app.snake[0].y < 0 |
| 70 | || app.snake[0].y >= game_size { |
| 71 | app.reset_game() |
| 72 | } |
| 73 | progress := f32_min(1, f32(time.ticks() - app.last_tick) / f32(tick_rate_ms)) |
| 74 | |
| 75 | // draw everything: |
| 76 | app.gg.begin() |
| 77 | // draw food |
| 78 | app.gg.draw_rect_filled(tile_size * app.food.x, tile_size * app.food.y + top_height, tile_size, |
| 79 | tile_size, gg.red) |
| 80 | |
| 81 | // draw snake |
| 82 | for pos in app.snake[..app.snake.len - 1] { |
| 83 | app.gg.draw_rect_filled(tile_size * pos.x, tile_size * pos.y + top_height, tile_size, |
| 84 | tile_size, gg.blue) |
| 85 | } |
| 86 | |
| 87 | // draw partial head |
| 88 | head := app.snake[0] |
| 89 | app.gg.draw_rect_filled(tile_size * (head.x + app.dir.x * progress), |
| 90 | |
| 91 | tile_size * (head.y + app.dir.y * progress) + top_height, tile_size, tile_size, gg.blue) |
| 92 | |
| 93 | // draw partial tail |
| 94 | tail := app.snake.last() |
| 95 | tail_dir := app.snake[app.snake.len - 2] - tail |
| 96 | app.gg.draw_rect_filled(tile_size * (tail.x + tail_dir.x * progress), |
| 97 | |
| 98 | tile_size * (tail.y + tail_dir.y * progress) + top_height, tile_size, tile_size, gg.blue) |
| 99 | |
| 100 | // draw score bar |
| 101 | app.gg.draw_rect_filled(0, 0, canvas_size, top_height, gg.black) |
| 102 | app.gg.draw_text(150, top_height / 2, 'Score: ${app.score}', gg.TextCfg{ |
| 103 | color: gg.white |
| 104 | align: .center |
| 105 | vertical_align: .middle |
| 106 | size: 65 |
| 107 | }) |
| 108 | app.gg.draw_text(canvas_size - 150, top_height / 2, 'Best: ${app.best}', gg.TextCfg{ |
| 109 | color: gg.white |
| 110 | align: .center |
| 111 | vertical_align: .middle |
| 112 | size: 65 |
| 113 | }) |
| 114 | |
| 115 | if progress == 1 { |
| 116 | // "snake" along |
| 117 | mut prev := app.snake[0] |
| 118 | app.snake[0] = app.snake[0] + app.dir |
| 119 | for i in 1 .. app.snake.len { |
| 120 | app.snake[i], prev = prev, app.snake[i] |
| 121 | } |
| 122 | |
| 123 | // add tail segment if food has been eaten |
| 124 | if app.snake[0] == app.food { |
| 125 | app.score++ |
| 126 | if app.score > app.best { |
| 127 | app.best = app.score |
| 128 | app.best.save() |
| 129 | } |
| 130 | app.snake << app.snake.last() + app.snake.last() - app.snake[app.snake.len - 2] |
| 131 | app.move_food() |
| 132 | } |
| 133 | |
| 134 | if app.dir_queue.len > 0 { |
| 135 | dir := app.dir_queue.pop() |
| 136 | if dir.x != -app.dir.x || dir.y != -app.dir.y { |
| 137 | app.dir = dir |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | app.last_tick = time.ticks() |
| 142 | } |
| 143 | |
| 144 | app.gg.end() |
| 145 | } |
| 146 | |
| 147 | // events |
| 148 | fn on_keydown(key gg.KeyCode, _mod gg.Modifier, mut app App) { |
| 149 | app.dir_queue << match key { |
| 150 | .w, .up { |
| 151 | Vec{0, -1} |
| 152 | } |
| 153 | .s, .down { |
| 154 | Vec{0, 1} |
| 155 | } |
| 156 | .a, .left { |
| 157 | Vec{-1, 0} |
| 158 | } |
| 159 | .d, .right { |
| 160 | Vec{1, 0} |
| 161 | } |
| 162 | else { |
| 163 | return |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | mut app := App{} |
| 169 | app.reset_game() |
| 170 | app.best.load() |
| 171 | |
| 172 | mut font_copy := font |
| 173 | app.gg = gg.new_context( |
| 174 | bg_color: gg.white |
| 175 | frame_fn: on_frame |
| 176 | keydown_fn: on_keydown |
| 177 | user_data: &app |
| 178 | width: canvas_size |
| 179 | height: top_height + canvas_size |
| 180 | window_title: 'snek' |
| 181 | font_bytes_normal: unsafe { font_copy.data().vbytes(font_copy.len) } |
| 182 | ) |
| 183 | app.gg.run() |
| 184 | |