| 1 | import os |
| 2 | import os.asset |
| 3 | import gg |
| 4 | |
| 5 | const csize = 32 |
| 6 | |
| 7 | struct Pos { |
| 8 | x int |
| 9 | y int |
| 10 | } |
| 11 | |
| 12 | enum Direction { |
| 13 | up |
| 14 | down |
| 15 | left |
| 16 | right |
| 17 | } |
| 18 | |
| 19 | @[heap] |
| 20 | struct Game { |
| 21 | mut: |
| 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 | |
| 44 | fn (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 |
| 100 | fn (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 | |
| 105 | fn (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 | |
| 144 | fn (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 | |
| 165 | fn (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 | |
| 171 | fn (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 | |
| 209 | fn (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 | |
| 218 | fn (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 | |
| 251 | fn (mut g Game) iid(name string) !int { |
| 252 | return g.ctx.create_image(asset.get_path('/', name))!.id |
| 253 | } |
| 254 | |
| 255 | fn 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 | |