v / examples / sokoban / sokoban.v
292 lines · 275 sloc · 6.49 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1import os
2import os.asset
3import gg
4
5const csize = 32
6
7struct Pos {
8 x int
9 y int
10}
11
12enum Direction {
13 up
14 down
15 left
16 right
17}
18
19@[heap]
20struct Game {
21mut:
22 warehouse [][]rune // the warehouse: `#`=wall, ` `=floor, `@`=storage, `b`=box, `p`=player
23 ww int
24 wh int
25 player Pos
26 boxes []Pos
27 win bool
28 level int
29 titles []string
30 levels []string
31 moves int
32 pushes int
33 ctx &gg.Context = unsafe { nil }
34 //
35 id_box int
36 id_box_on_storage int
37 id_wall int
38 id_floor int
39 id_other int
40 id_player int
41 id_storage int
42}
43
44fn (mut g Game) parse_level(lnumber int) ! {
45 level := g.levels[lnumber] or { return error('invalid level number ${lnumber}') }
46 if level == '' {
47 return error('empty level ${lnumber}')
48 }
49 lines := level.split('\n').map(it.trim_space_right()).filter(it != '' && !it.starts_with('//'))
50 mut warehouse := [][]rune{}
51 mut warehouse_w := 0
52 mut player := Pos{-1, -1}
53 mut boxes := []Pos{}
54 for y, line in lines#[1..] {
55 mut row := []rune{}
56 for x, c in line {
57 match c {
58 `#`, ` ` {
59 row << c
60 }
61 `b`, `$` { // a normal box
62 row << ` `
63 boxes << Pos{x, y}
64 }
65 `p`, `@` { // a normal player
66 row << ` `
67 player = Pos{x, y}
68 }
69 `.` { // storage
70 row << `@`
71 }
72 `*` { // box on storage
73 row << `@`
74 boxes << Pos{x, y}
75 }
76 `+` { // player on storage
77 row << `@`
78 player = Pos{x, y}
79 }
80 else {
81 return error('unknown rune `${rune(c)}` at position ${y}x${x}, in level: ${c}')
82 }
83 }
84 }
85 warehouse << row
86 warehouse_w = int_max(row.len, warehouse_w)
87 }
88 g.warehouse = warehouse
89 g.ww = warehouse_w * csize
90 g.wh = warehouse.len * csize
91 g.titles = lines[0].split('@').filter(it != '')
92 g.player = player
93 g.boxes = boxes
94 g.moves = 0
95 g.pushes = 0
96 g.win = false
97}
98
99// is_valid_pos returns whether the given `pos` is inside the game field, and not inside an inner wall
100fn (g &Game) is_valid_pos(pos Pos) bool {
101 return pos.y >= 0 && pos.y < g.warehouse.len && pos.x >= 0 && pos.x < g.warehouse[pos.y].len
102 && g.warehouse[pos.y][pos.x] != `#`
103}
104
105fn (mut g Game) move_player(dir Direction) {
106 dx, dy := match dir {
107 .up { 0, -1 }
108 .down { 0, 1 }
109 .left { -1, 0 }
110 .right { 1, 0 }
111 }
112
113 target := Pos{g.player.x + dx, g.player.y + dy}
114 if !g.is_valid_pos(target) {
115 return
116 }
117 // Check for a box at the target position:
118 mut target_box_index := -1
119 for i, box in g.boxes {
120 if box.x == target.x && box.y == target.y {
121 target_box_index = i
122 break
123 }
124 }
125 if target_box_index >= 0 {
126 // try pushing the box
127 target_after := Pos{g.player.x + 2 * dx, g.player.y + 2 * dy}
128 if !g.is_valid_pos(target_after) {
129 return
130 }
131 // if there is another box at that place, prevent the push:
132 for box in g.boxes {
133 if box.x == target_after.x && box.y == target_after.y {
134 return
135 }
136 }
137 g.boxes[target_box_index] = target_after
138 g.pushes++
139 }
140 g.player = target
141 g.moves++
142}
143
144fn (g &Game) get_cell_iid(pos Pos) int {
145 c := g.warehouse[pos.y][pos.x]
146 if pos.x == g.player.x && pos.y == g.player.y {
147 return g.id_player
148 }
149 for box in g.boxes {
150 if box.x == pos.x && box.y == pos.y {
151 if c == `@` {
152 return g.id_box_on_storage
153 }
154 return g.id_box
155 }
156 }
157 match c {
158 ` ` { return g.id_floor }
159 `#` { return g.id_wall }
160 `@` { return g.id_storage }
161 else { return g.id_other }
162 }
163}
164
165fn (mut g Game) next_level() {
166 nlevel := (g.level + 1) % g.levels.len
167 g.parse_level(nlevel) or { eprintln('level error: ${err}') }
168 g.level = nlevel
169}
170
171fn (mut g Game) key_down(key gg.KeyCode, _ gg.Modifier, _ voidptr) {
172 // controls:
173 match key {
174 .escape {
175 g.ctx.quit()
176 }
177 .space {
178 if g.win {
179 g.next_level()
180 return
181 }
182 }
183 .r {
184 g.parse_level(g.level) or {}
185 return
186 }
187 .n {
188 g.next_level()
189 }
190 else {}
191 }
192
193 if g.win {
194 return
195 }
196 // player movement:
197 dir := match key {
198 .w, .up { Direction.up }
199 .s, .down { Direction.down }
200 .a, .left { Direction.left }
201 .d, .right { Direction.right }
202 else { return }
203 }
204
205 g.move_player(dir)
206 g.win = g.boxes.all(g.warehouse[it.y][it.x] == `@`)
207}
208
209fn (g &Game) ctext(ws gg.Size, oy int, message string, size int, color gg.Color) {
210 g.ctx.draw_text(ws.width / 2, ws.height + oy, message,
211 color: color
212 size: size
213 align: .center
214 vertical_align: .middle
215 )
216}
217
218fn (g &Game) draw_frame(_ voidptr) {
219 g.ctx.begin()
220 ws := gg.window_size()
221 ox := (ws.width - g.ww) / 2
222 oy := (ws.height - 40 - g.wh) / 2
223 for y in 0 .. g.warehouse.len {
224 for x in 0 .. g.warehouse[y].len {
225 pos := Pos{x, y}
226 iid := g.get_cell_iid(pos)
227 if iid == g.id_player {
228 // the player is transparent
229 g.ctx.draw_image_by_id(ox + x * csize, oy + y * csize, 32, 32, g.id_floor)
230 }
231 g.ctx.draw_image_by_id(ox + x * csize, oy + y * csize, 32, 32, iid)
232 }
233 }
234 g.ctx.draw_rect_filled(0, ws.height - 70, ws.width, 70, gg.black)
235 if g.win {
236 g.ctext(ws, -50, 'You win!!!', 60, gg.yellow)
237 g.ctext(ws, -15, 'Press `space` to continue.', 20, gg.gray)
238 } else {
239 for idx, title in g.titles {
240 g.ctext(ws, -65 + (idx * 20), title, 22, gg.white)
241 }
242 g.ctext(ws, -65 + (g.titles.len * 20), 'Boxes: ${g.boxes.len:04}', 16, gg.gray)
243 }
244 g.ctx.draw_rect_filled(0, 0, ws.width, 40, gg.black)
245 g.ctx.draw_text(30, 0, 'Level: ${g.level + 1:02}', color: gg.green, size: 40)
246 g.ctx.draw_text(ws.width - 225, 0, 'Moves: ${g.moves:04}', color: gg.green, size: 40)
247 g.ctx.draw_text(ws.width / 2 - 110, 0, 'Pushes: ${g.pushes:04}', color: gg.green, size: 40)
248 g.ctx.end()
249}
250
251fn (mut g Game) iid(name string) !int {
252 return g.ctx.create_image(asset.get_path('/', name))!.id
253}
254
255fn main() {
256 mut g := &Game{}
257 if os.args.len > 1 {
258 for fpath in os.args[1..] {
259 content := os.read_file(fpath)!
260 if content.starts_with(';') {
261 // many levels in the same file:
262 parts := content.trim_space().split('\n\n')
263 for part in parts {
264 g.levels << '${fpath}${part}'
265 }
266 } else {
267 // a single level:
268 g.levels << content
269 }
270 }
271 } else {
272 all_level_names := asset.read_text('/', '_all_levels.txt')!.split_into_lines()
273 g.levels = all_level_names.map(asset.read_text('/', it)!)
274 }
275 g.parse_level(0)!
276 g.ctx = gg.new_context(
277 width: 800
278 height: 640
279 window_title: 'V Sokoban'
280 user_data: g
281 frame_fn: g.draw_frame
282 keydown_fn: g.key_down
283 )
284 g.id_box = g.iid('box.png')!
285 g.id_box_on_storage = g.iid('box_on_storage.png')!
286 g.id_wall = g.iid('wall.png')!
287 g.id_floor = g.iid('floor.png')!
288 g.id_other = g.iid('other.png')!
289 g.id_player = g.iid('player.png')!
290 g.id_storage = g.iid('storage.png')!
291 g.ctx.run()
292}
293