| 1 | // Copyright (c) 2026 Delyan Angelov. All rights reserved. |
| 2 | // The use of this source code is governed by the MIT license. |
| 3 | module main |
| 4 | |
| 5 | import gg |
| 6 | import os.asset |
| 7 | import rand |
| 8 | |
| 9 | const n = 4 |
| 10 | const tile = 110 |
| 11 | const gap = 10 |
| 12 | const pad = 24 |
| 13 | const head = 52 |
| 14 | const r = f32(18) |
| 15 | const speed = f32(0.14) |
| 16 | const board_px = n * tile + (n - 1) * gap |
| 17 | const width = pad * 2 + board_px |
| 18 | const height = head + board_px + 62 |
| 19 | const wcolor = gg.rgb(104, 93, 79) |
| 20 | |
| 21 | @[heap] |
| 22 | struct Game { |
| 23 | mut: |
| 24 | ctx &gg.Context = unsafe { nil } |
| 25 | board []int |
| 26 | moves int |
| 27 | won bool |
| 28 | anim bool |
| 29 | t f32 |
| 30 | from int = -1 |
| 31 | to int = -1 |
| 32 | val int |
| 33 | } |
| 34 | |
| 35 | fn main() { |
| 36 | mut g := &Game{} |
| 37 | g.shuffle() |
| 38 | g.ctx = gg.new_context( |
| 39 | bg_color: gg.rgb(247, 244, 235) |
| 40 | width: width |
| 41 | height: height |
| 42 | sample_count: 4 |
| 43 | window_title: '15 Puzzle' |
| 44 | user_data: g |
| 45 | frame_fn: frame |
| 46 | event_fn: event |
| 47 | font_path: asset.get_path('../assets', 'fonts/Graduate-Regular.ttf') |
| 48 | ) |
| 49 | g.ctx.run() |
| 50 | } |
| 51 | |
| 52 | fn frame(mut g Game) { |
| 53 | g.step() |
| 54 | g.ctx.begin() |
| 55 | g.header() |
| 56 | g.board() |
| 57 | g.ctx.end() |
| 58 | } |
| 59 | |
| 60 | fn event(e &gg.Event, mut g Game) { |
| 61 | if e.typ == .key_down { |
| 62 | match e.key_code { |
| 63 | .escape { |
| 64 | g.ctx.quit() |
| 65 | } |
| 66 | .r { |
| 67 | g.shuffle() |
| 68 | } |
| 69 | .up, .w { |
| 70 | if !g.anim { |
| 71 | g.slide(1, 0) |
| 72 | } |
| 73 | } |
| 74 | .down, .s { |
| 75 | if !g.anim { |
| 76 | g.slide(-1, 0) |
| 77 | } |
| 78 | } |
| 79 | .left, .a { |
| 80 | if !g.anim { |
| 81 | g.slide(0, 1) |
| 82 | } |
| 83 | } |
| 84 | .right, .d { |
| 85 | if !g.anim { |
| 86 | g.slide(0, -1) |
| 87 | } |
| 88 | } |
| 89 | else {} |
| 90 | } |
| 91 | |
| 92 | return |
| 93 | } |
| 94 | if !g.anim && e.typ == .mouse_down && e.mouse_button == .left { |
| 95 | if i := g.hit(int(e.mouse_x), int(e.mouse_y)) { |
| 96 | g.move(i) |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | fn (mut g Game) header() { |
| 102 | g.ctx.draw_text(width / 2, 16, if g.won { |
| 103 | 'Solved in ${g.moves} moves. Press R to reshuffle.' |
| 104 | } else { |
| 105 | 'Moves: ${g.moves}' |
| 106 | }, |
| 107 | size: if g.won { 16 } else { 20 } |
| 108 | bold: true |
| 109 | align: .center |
| 110 | color: if g.won { wcolor } else { gg.rgb(48, 44, 37) } |
| 111 | ) |
| 112 | } |
| 113 | |
| 114 | fn (mut g Game) board() { |
| 115 | g.ctx.draw_rect_filled(pad - 8, head - 8, board_px + 16, board_px + 16, gg.rgb(218, 206, 188)) |
| 116 | for i, v in g.board { |
| 117 | if g.anim && i == g.from { |
| 118 | continue |
| 119 | } |
| 120 | x, y := xy(i) |
| 121 | g.tile(x, y, v, v != 0 && v == i + 1) |
| 122 | } |
| 123 | if g.anim { |
| 124 | x0, y0 := xy(g.from) |
| 125 | x1, y1 := xy(g.to) |
| 126 | g.tile(int(f32(x0) + f32(x1 - x0) * g.t), int(f32(y0) + f32(y1 - y0) * g.t), g.val, false) |
| 127 | } |
| 128 | g.ctx.draw_text(pad, head + board_px + 22, |
| 129 | 'Arrow keys / WASD or click a tile next to the empty space.', |
| 130 | size: 16 |
| 131 | color: wcolor |
| 132 | ) |
| 133 | g.ctx.draw_text(width / 2, height - 18, '[R] Shuffle [Esc] Quit', |
| 134 | size: 16 |
| 135 | bold: true |
| 136 | align: .center |
| 137 | vertical_align: .middle |
| 138 | color: wcolor |
| 139 | ) |
| 140 | } |
| 141 | |
| 142 | fn (mut g Game) tile(x int, y int, v int, ok bool) { |
| 143 | fill, border := if v == 0 { |
| 144 | gg.rgb(238, 232, 220), gg.rgb(210, 202, 191) |
| 145 | } else if ok { |
| 146 | gg.rgb(214, 173, 108), gg.rgb(168, 121, 56) |
| 147 | } else { |
| 148 | gg.rgb(84, 110, 122), gg.rgb(48, 66, 74) |
| 149 | } |
| 150 | g.ctx.draw_rounded_rect_filled(x, y, tile, tile, r, fill) |
| 151 | g.ctx.draw_rounded_rect_empty(x, y, tile, tile, r, border) |
| 152 | if v != 0 { |
| 153 | g.ctx.draw_text(x + tile / 2, y + tile / 2, v.str(), |
| 154 | size: 42 |
| 155 | bold: true |
| 156 | align: .center |
| 157 | vertical_align: .middle |
| 158 | color: gg.white |
| 159 | ) |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | fn (mut g Game) shuffle() { |
| 164 | g.board = []int{len: n * n} |
| 165 | for i in 0 .. g.board.len - 1 { |
| 166 | g.board[i] = i + 1 |
| 167 | } |
| 168 | for _ in 0 .. 300 { |
| 169 | e := g.board.index(0) |
| 170 | ns := neighbors(e) |
| 171 | j := ns[rand.intn(ns.len) or { 0 }] |
| 172 | g.board[e], g.board[j] = g.board[j], g.board[e] |
| 173 | } |
| 174 | if done(g.board) { |
| 175 | g.board[g.board.len - 2], g.board[g.board.len - 3] = g.board[g.board.len - 3], g.board[g.board.len - 2] |
| 176 | } |
| 177 | g.moves, g.won, g.anim, g.t, g.from, g.to, g.val = 0, false, false, 0, -1, -1, 0 |
| 178 | } |
| 179 | |
| 180 | fn (mut g Game) slide(dr int, dc int) { |
| 181 | e := g.board.index(0) |
| 182 | row, col := e / n + dr, e % n + dc |
| 183 | if row >= 0 && row < n && col >= 0 && col < n { |
| 184 | g.move(row * n + col) |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | fn (mut g Game) move(i int) bool { |
| 189 | if g.won || g.anim || i < 0 || i >= g.board.len || g.board[i] == 0 { |
| 190 | return false |
| 191 | } |
| 192 | e := g.board.index(0) |
| 193 | dr, dc := i / n - e / n, i % n - e % n |
| 194 | if !((dr == 0 && (dc == 1 || dc == -1)) || (dc == 0 && (dr == 1 || dr == -1))) { |
| 195 | return false |
| 196 | } |
| 197 | g.anim, g.t, g.from, g.to, g.val = true, 0, i, e, g.board[i] |
| 198 | return true |
| 199 | } |
| 200 | |
| 201 | fn (mut g Game) step() { |
| 202 | if !g.anim { |
| 203 | return |
| 204 | } |
| 205 | g.t += speed |
| 206 | if g.t < 1 { |
| 207 | return |
| 208 | } |
| 209 | g.board[g.to], g.board[g.from] = g.val, 0 |
| 210 | g.moves++ |
| 211 | g.won = done(g.board) |
| 212 | g.anim, g.t, g.from, g.to, g.val = false, 0, -1, -1, 0 |
| 213 | } |
| 214 | |
| 215 | fn (g &Game) hit(mx int, my int) ?int { |
| 216 | if mx < pad || my < head || mx >= pad + board_px || my >= head + board_px { |
| 217 | return none |
| 218 | } |
| 219 | x, y := mx - pad, my - head |
| 220 | col, row := x / (tile + gap), y / (tile + gap) |
| 221 | if x % (tile + gap) >= tile || y % (tile + gap) >= tile { |
| 222 | return none |
| 223 | } |
| 224 | return row * n + col |
| 225 | } |
| 226 | |
| 227 | fn neighbors(i int) []int { |
| 228 | row, col := i / n, i % n |
| 229 | mut ns := []int{} |
| 230 | if row > 0 { |
| 231 | ns << i - n |
| 232 | } |
| 233 | if row + 1 < n { |
| 234 | ns << i + n |
| 235 | } |
| 236 | if col > 0 { |
| 237 | ns << i - 1 |
| 238 | } |
| 239 | if col + 1 < n { |
| 240 | ns << i + 1 |
| 241 | } |
| 242 | return ns |
| 243 | } |
| 244 | |
| 245 | fn xy(i int) (int, int) { |
| 246 | return pad + (i % n) * (tile + gap), head + (i / n) * (tile + gap) |
| 247 | } |
| 248 | |
| 249 | fn done(b []int) bool { |
| 250 | for i in 0 .. b.len - 1 { |
| 251 | if b[i] != i + 1 { |
| 252 | return false |
| 253 | } |
| 254 | } |
| 255 | return b[b.len - 1] == 0 |
| 256 | } |
| 257 | |