lispjam-autumn-2024/notes.md

10 KiB

First, establish some of the cell data

; This will result in a "2x2" cell, of [0 1 1 1], with 0 representing an empty
; space, and 1 representing a wall.
(var (hall-width wall-width) (values 1 1))

; This sets the "map width". This means 10 cells across and down. With each cell
; being 2x2, this results in a 20x20 square map. All maps will be square (for
; now).
(var num-cells 10)

; Explicitly setting cell-size for convenience. All cells are squares
(var cell-size (+ hall-width wall-width))

Next, generate the initial array of cells to work with.

(var cells {})
(for [i 1 (* num-cells num-cells)]
    (table.insert cells {
        :n false :s false :e false :w false 
        :v false 
        :o false
        }))
`:n`, `:s`, `:e`, `:w` - if cell has a directional connection
`:v` - has the cell been visited
`:o` - does the cell contain an obstacle

With the cells established, can use a recursive depth-first search with a stack to generate the maze.

For ease, we'll always start along the top. Eventually, want to ensure the solution is along the sides or bottom.

(var cell-stack [(math.random 1 num-cells)])

(while (> 0 (length cell-stack)
    (var current-cell (table.remove cell-stack))
    (tset cells current-cell :v true)

    ; Check if any of the current cell's neighbors are
    ;   1. unvisited
    ;   2. a side/barrier wall
    (var next-cells {})

    ; North - 
    ;   if current-cell <= num-cells, then north is a map-side/barrier
    (when (> num-cells current-cell)
        (var n-cell (- current-cell num-cells))
        (if (not (. cells n-cell :v)) (table.insert next-cells n-cell)))

    ; South - 
    ;   if current-cell > (- (* num-cells num-cells) num-cells), 
    ;       then south is a map-side/barrier
    (when (< current-cell (- (* num-cells num-cells) num-cells))
        (var s-cell (- (* num-cells num-cells) num-cells))
        (if (not (. cells s-cell :v)) (table.insert next-cells s-cell)))

    ; East -
    ;   if current-cell % num-cells = 0, then east is a map-side/barrier
    (when (not (= 0 (% current-cell num-cells)))
        (var e-cell (+ current-cell 1))
        (if (not (. cells e-cell :v)) (table.insert next-cells e-cell)))

    ; West -
    ;   if current-cell % num-cells = 1, then west is a map-side/barrier
    (when (not (= 1 (% current-cell num-cells)))
        (var w-cell (- current-cell 1))
        (if (not (. cells e-cell :v)) (table.insert next-cells w-cell)))


For output:

Each cell is then used to generate values in the map array. Each cell will have either a hallway or a wall at each position, for cell-size positions.

Iterate through each cell. Cells 1 through num-cells are the first row, then (1 + num-cells) through (num-cells * 2), and so on. This can be generalized and extrapolated to (x + (num-cells * (y - 1))) through (num-cells * y), for x and y loops from 1 to num-cells.

(for [i 1 num-cells]
    (for [j 1 num-cells]

Each cell is of uniform size, and the map is a square that divides evenly by that size. We established the width, and that value squared is the map. Every cell is numbered from 1 to (* num-cells num-cells) (ie., 1 to 100). The index of the cell divided by num-cells, plus 1, gets the row:

(fn row [i] (+ 1 (// i num-cells)))

This gets the "upper-left" for the map array, given a cell index.

(fn c [i] (+ (- (* i cell-size) 1) (* cell-size cell-num (- (row i) 1))))

Using that index, iterate through the cell based on cell-size twice, using each loop to add either (- i 1) and (* (- j 1) cell-num) to the value from c above, and insert it into the map array.


For cell generation:

Each cell is a combination of hallways and walls. For each cell, if the "row" or "column" is less than the hall-width, enter a "0", otherwise enter a "1".

(var cell [])
(each [k v (ipairs meta-cells)]
    (var row-limit (if (. v :e) (+ hall-width wall-width) hall-width))
    (var col-limit (if (. v :s) (+ hall-width wall-width) hall-width))
    (for [i 1 cell-size]
        (for [j 1 cell-size]
            (var cell-index (+ j (* (- i 1) cell-size)))
            (if (and (< i (+ col-limit 1)) (< j (+ row-limit 1)))
                    (tset cell cell-index 0)
                    (tset cell cell-index 1))
            (if (and (> i hall-width) (> j hall-width))
                    (tset cell cell-index 1))))

This becomes a bit more challenging with considering connections. It is likely a matter of modifying the conditional such that: when there is a horizontal connection, check for i to be less than the entire cell width (hall-width + wall-width); and then similar for j with vertical connections. However, this will then remove the corner wall when there are both vertical and horizontal connections. This could be solved by checking if both i and j are beyond hall-width, which would represent the always corner.

The above code should translate from a meta-cells list of meta-data to a cells list of 0's and 1's.


With a bit of modification, the logic for the maze generation from above appears to work. There is one challenge which remains, which is updating the walls. A way around this is to make the data about the next cell more verbose: include not only the cell number, but also the direction. Then, use a case statement to update accordingly.

The last remaining tasks are to buffer the entire maze, such that there are two cells worth of walls around it; and to establish starting and ending squares.

Creating the buffer means adding two cells worth of walls to the north and west walls, and 1 cell worth of walls to the south and east walls. This should theoretically be very easy using table.insert, which automatically modifies all further table values. However, will need to be mindful when updating east and west. Updating north is just (table.insert 1 1) for (* cell-num cell-size cell-size) times twice. Similar for south, except (table.insert 1) for (* cell-num cell-size cell-size) once. East and west seem more challenging. Though, thinking about it for a moment, I could essentially migrate the generated array one "row" at a time into a new array, padding it on either side as I do so. I believe this may be the way. I can use table.move to accomplish this.

As part of the padding, I also need to translate the map from a single list into a list of lists. Again, this can be done using the table.move function and table.insert.

(var map [])
(var map-row [])
(table.move cell_map x y 1 map-row)
(table.insert map map-row)

Remaining tasks:

  1. Finalize map generation: [X] Establish starting spot, and add the northern feature spawn point. [X] Establish ending spot, and add the southern feature finale point. [O] Modify the map wall values to account for random wall heights. - This "works", but the walls are all only the front of the square, so it looks odd.
  2. Draw floors
  3. Draw sky-boxen
  4. Add monster mechanics

Thinking about a HUD. On the top can be a compass, and then on the bottom can be a few additional things.

For the compass, I know the direction of the player. I've implemented the state function again, and am writing that data to it and reading from it for the overlay. When the player.getX is greater than 0, I'm "facing" north; when less than 0, facing south. For the player.getY, greater than 0 is west; less than is east. It's easy to just print the value, but it'd be really cool to instead print one of those compass bars. Theoretically it's printing out the elements of a list at set screen intervals. The list may look like:

(var compass-bar [":" "|" "N" "|" ":" "|" "E" "|" ":" "|" "S" "|" ":" "|" "W" "|"])

Then it would print 5 elements, starting at x, going to x + 5, wrapping around. x would be determined based on the two values of player.getX and player.getY.

This will ensure we loop properly through the indexes, assuming x is always larger than the array length.

(fn fi [x] (set r x) (while (> r 5) (set r (- r 5))) r)

Beyond this, it may be good to pair this with logic that allows adding or subtracting from the index and not escaping the list. This would basically check if the new index will be either less-than 0 or greater than the list length, and adjust accordingly.

Could be something like:

(if x > 0) // northern half
    index is set to north
    (adjust index by ratio of y to (-1 .. 1))
(else) // southern half
    index is set to south
    (adjust index by ratio y to (-1 .. 1))

The adjustment algorithm is the same, and it's either added or subtracted from the index, based on which hemisphere, probably?

This value (math.floor (+ 10 (* 10 (player.getDirX)))) yields 0-19 in both an east and west facing arc. If I use east and west as a switch, then when I'm facing east I can move the compass bar list in one direction, and the other when facing west.

In order to do this I need a proper circular-list approach.

(var compass [<elements>])
(fn circular-compass [c n]
    (if (< (+ c n) 1) (length compass)
        (> (length compass) (+ c n)) 0
        (+ c n))

Compass bar "works", at least well enough for now. Time to move to some menus. I think a cool aesthetic would be the start menu being like the inside of the escape pod you start outside of. The background would be a texture of some kind of metallic wall, and there could be a flashing light effect. The menu items would be the configuration for the maze: size, number of survivors, etc.. There would be a button to start, and then a button to quit. If I have time, there can be a button to remap keys, and maybe turn sound on and off.

Focusing on the menu buttons themselves, each one has a shape and text. Additionally, each one should have a hover effect. Each menu can have a left-action, right-action, and select-action.

(var menu-font (love.graphics.newFont 20))
(fn menu-button [x y text la ra sa]
    (love.graphics.polygon ...)
    (love.graphics.printf text menu-font x (+ y ...) (+ x ...))
    (var (left-area right-area mid-area) (values  ...))
    (when (left-area) (la))
    (when (right-area) (ra))
    (when (mid-area) (sa))

There should also be a way to navigate with the keyboard. Using up and down arrows or keys will move the "selected" button, left/right arrows/keys will activate the left/right actions, and enter/use will activate the select action.

Start simple, make the polygon a square, make the areas square. Add buttons for survivors and supplies and monster and stuff, even though it's not implemented yet.