v2 / examples / gg / 15.v
256 lines · 238 sloc · 5.09 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1// Copyright (c) 2026 Delyan Angelov. All rights reserved.
2// The use of this source code is governed by the MIT license.
3module main
4
5import gg
6import os.asset
7import rand
8
9const n = 4
10const tile = 110
11const gap = 10
12const pad = 24
13const head = 52
14const r = f32(18)
15const speed = f32(0.14)
16const board_px = n * tile + (n - 1) * gap
17const width = pad * 2 + board_px
18const height = head + board_px + 62
19const wcolor = gg.rgb(104, 93, 79)
20
21@[heap]
22struct Game {
23mut:
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
35fn 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
52fn frame(mut g Game) {
53 g.step()
54 g.ctx.begin()
55 g.header()
56 g.board()
57 g.ctx.end()
58}
59
60fn 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
101fn (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
114fn (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
142fn (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
163fn (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
180fn (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
188fn (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
201fn (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
215fn (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
227fn 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
245fn xy(i int) (int, int) {
246 return pad + (i % n) * (tile + gap), head + (i / n) * (tile + gap)
247}
248
249fn 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