257 lines
5.1 KiB
V
257 lines
5.1 KiB
V
// Copyright (c) 2026 Delyan Angelov. All rights reserved.
|
|
// The use of this source code is governed by the MIT license.
|
|
module main
|
|
|
|
import gg
|
|
import os.asset
|
|
import rand
|
|
|
|
const n = 4
|
|
const tile = 110
|
|
const gap = 10
|
|
const pad = 24
|
|
const head = 52
|
|
const r = f32(18)
|
|
const speed = f32(0.14)
|
|
const board_px = n * tile + (n - 1) * gap
|
|
const width = pad * 2 + board_px
|
|
const height = head + board_px + 62
|
|
const wcolor = gg.rgb(104, 93, 79)
|
|
|
|
@[heap]
|
|
struct Game {
|
|
mut:
|
|
ctx &gg.Context = unsafe { nil }
|
|
board []int
|
|
moves int
|
|
won bool
|
|
anim bool
|
|
t f32
|
|
from int = -1
|
|
to int = -1
|
|
val int
|
|
}
|
|
|
|
fn main() {
|
|
mut g := &Game{}
|
|
g.shuffle()
|
|
g.ctx = gg.new_context(
|
|
bg_color: gg.rgb(247, 244, 235)
|
|
width: width
|
|
height: height
|
|
sample_count: 4
|
|
window_title: '15 Puzzle'
|
|
user_data: g
|
|
frame_fn: frame
|
|
event_fn: event
|
|
font_path: asset.get_path('../assets', 'fonts/Graduate-Regular.ttf')
|
|
)
|
|
g.ctx.run()
|
|
}
|
|
|
|
fn frame(mut g Game) {
|
|
g.step()
|
|
g.ctx.begin()
|
|
g.header()
|
|
g.board()
|
|
g.ctx.end()
|
|
}
|
|
|
|
fn event(e &gg.Event, mut g Game) {
|
|
if e.typ == .key_down {
|
|
match e.key_code {
|
|
.escape {
|
|
g.ctx.quit()
|
|
}
|
|
.r {
|
|
g.shuffle()
|
|
}
|
|
.up, .w {
|
|
if !g.anim {
|
|
g.slide(1, 0)
|
|
}
|
|
}
|
|
.down, .s {
|
|
if !g.anim {
|
|
g.slide(-1, 0)
|
|
}
|
|
}
|
|
.left, .a {
|
|
if !g.anim {
|
|
g.slide(0, 1)
|
|
}
|
|
}
|
|
.right, .d {
|
|
if !g.anim {
|
|
g.slide(0, -1)
|
|
}
|
|
}
|
|
else {}
|
|
}
|
|
|
|
return
|
|
}
|
|
if !g.anim && e.typ == .mouse_down && e.mouse_button == .left {
|
|
if i := g.hit(int(e.mouse_x), int(e.mouse_y)) {
|
|
g.move(i)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn (mut g Game) header() {
|
|
g.ctx.draw_text(width / 2, 16, if g.won {
|
|
'Solved in ${g.moves} moves. Press R to reshuffle.'
|
|
} else {
|
|
'Moves: ${g.moves}'
|
|
},
|
|
size: if g.won { 16 } else { 20 }
|
|
bold: true
|
|
align: .center
|
|
color: if g.won { wcolor } else { gg.rgb(48, 44, 37) }
|
|
)
|
|
}
|
|
|
|
fn (mut g Game) board() {
|
|
g.ctx.draw_rect_filled(pad - 8, head - 8, board_px + 16, board_px + 16, gg.rgb(218, 206, 188))
|
|
for i, v in g.board {
|
|
if g.anim && i == g.from {
|
|
continue
|
|
}
|
|
x, y := xy(i)
|
|
g.tile(x, y, v, v != 0 && v == i + 1)
|
|
}
|
|
if g.anim {
|
|
x0, y0 := xy(g.from)
|
|
x1, y1 := xy(g.to)
|
|
g.tile(int(f32(x0) + f32(x1 - x0) * g.t), int(f32(y0) + f32(y1 - y0) * g.t), g.val, false)
|
|
}
|
|
g.ctx.draw_text(pad, head + board_px + 22,
|
|
'Arrow keys / WASD or click a tile next to the empty space.',
|
|
size: 16
|
|
color: wcolor
|
|
)
|
|
g.ctx.draw_text(width / 2, height - 18, '[R] Shuffle [Esc] Quit',
|
|
size: 16
|
|
bold: true
|
|
align: .center
|
|
vertical_align: .middle
|
|
color: wcolor
|
|
)
|
|
}
|
|
|
|
fn (mut g Game) tile(x int, y int, v int, ok bool) {
|
|
fill, border := if v == 0 {
|
|
gg.rgb(238, 232, 220), gg.rgb(210, 202, 191)
|
|
} else if ok {
|
|
gg.rgb(214, 173, 108), gg.rgb(168, 121, 56)
|
|
} else {
|
|
gg.rgb(84, 110, 122), gg.rgb(48, 66, 74)
|
|
}
|
|
g.ctx.draw_rounded_rect_filled(x, y, tile, tile, r, fill)
|
|
g.ctx.draw_rounded_rect_empty(x, y, tile, tile, r, border)
|
|
if v != 0 {
|
|
g.ctx.draw_text(x + tile / 2, y + tile / 2, v.str(),
|
|
size: 42
|
|
bold: true
|
|
align: .center
|
|
vertical_align: .middle
|
|
color: gg.white
|
|
)
|
|
}
|
|
}
|
|
|
|
fn (mut g Game) shuffle() {
|
|
g.board = []int{len: n * n}
|
|
for i in 0 .. g.board.len - 1 {
|
|
g.board[i] = i + 1
|
|
}
|
|
for _ in 0 .. 300 {
|
|
e := g.board.index(0)
|
|
ns := neighbors(e)
|
|
j := ns[rand.intn(ns.len) or { 0 }]
|
|
g.board[e], g.board[j] = g.board[j], g.board[e]
|
|
}
|
|
if done(g.board) {
|
|
g.board[g.board.len - 2], g.board[g.board.len - 3] = g.board[g.board.len - 3], g.board[g.board.len - 2]
|
|
}
|
|
g.moves, g.won, g.anim, g.t, g.from, g.to, g.val = 0, false, false, 0, -1, -1, 0
|
|
}
|
|
|
|
fn (mut g Game) slide(dr int, dc int) {
|
|
e := g.board.index(0)
|
|
row, col := e / n + dr, e % n + dc
|
|
if row >= 0 && row < n && col >= 0 && col < n {
|
|
g.move(row * n + col)
|
|
}
|
|
}
|
|
|
|
fn (mut g Game) move(i int) bool {
|
|
if g.won || g.anim || i < 0 || i >= g.board.len || g.board[i] == 0 {
|
|
return false
|
|
}
|
|
e := g.board.index(0)
|
|
dr, dc := i / n - e / n, i % n - e % n
|
|
if !((dr == 0 && (dc == 1 || dc == -1)) || (dc == 0 && (dr == 1 || dr == -1))) {
|
|
return false
|
|
}
|
|
g.anim, g.t, g.from, g.to, g.val = true, 0, i, e, g.board[i]
|
|
return true
|
|
}
|
|
|
|
fn (mut g Game) step() {
|
|
if !g.anim {
|
|
return
|
|
}
|
|
g.t += speed
|
|
if g.t < 1 {
|
|
return
|
|
}
|
|
g.board[g.to], g.board[g.from] = g.val, 0
|
|
g.moves++
|
|
g.won = done(g.board)
|
|
g.anim, g.t, g.from, g.to, g.val = false, 0, -1, -1, 0
|
|
}
|
|
|
|
fn (g &Game) hit(mx int, my int) ?int {
|
|
if mx < pad || my < head || mx >= pad + board_px || my >= head + board_px {
|
|
return none
|
|
}
|
|
x, y := mx - pad, my - head
|
|
col, row := x / (tile + gap), y / (tile + gap)
|
|
if x % (tile + gap) >= tile || y % (tile + gap) >= tile {
|
|
return none
|
|
}
|
|
return row * n + col
|
|
}
|
|
|
|
fn neighbors(i int) []int {
|
|
row, col := i / n, i % n
|
|
mut ns := []int{}
|
|
if row > 0 {
|
|
ns << i - n
|
|
}
|
|
if row + 1 < n {
|
|
ns << i + n
|
|
}
|
|
if col > 0 {
|
|
ns << i - 1
|
|
}
|
|
if col + 1 < n {
|
|
ns << i + 1
|
|
}
|
|
return ns
|
|
}
|
|
|
|
fn xy(i int) (int, int) {
|
|
return pad + (i % n) * (tile + gap), head + (i / n) * (tile + gap)
|
|
}
|
|
|
|
fn done(b []int) bool {
|
|
for i in 0 .. b.len - 1 {
|
|
if b[i] != i + 1 {
|
|
return false
|
|
}
|
|
}
|
|
return b[b.len - 1] == 0
|
|
}
|