Log In  


Hello,

I've been working on trying to produce a randomly generated map like in Slay the Spire.

I used the rules documented here to generate the map:

https://steamcommunity.com/sharedfiles/filedetails/?id=2830078257

(not convinced these are the whole rules but it's good enough for now)

The map is a 7 x 15 grid.
6 paths are drawn through the grid, the first two paths must be at different starting points, others are random (which gives the branching effect)
Events are placed on the grid as per the weightings specified.
Some rules are obeyed to prevent certain events from appearing in certain places (for example, 2 rest sites in a row are not allowed)
I added some jittering to give the map rendering a more organic look.

Controls

1) You can scroll up and down the map with the up and down keys.
2) Hit left and right to select an originating path. Press X to lock in the selection.
3) Press up to move up that selected path.
4) While you have a path selected, press left and right to move between valid branches of the path.

Seems to be working, although it's an awful lot of code, I need to look for some optimisations next.

Any comments, or suggestions welcome.

Cart #yuzuhutuba-0 | 2024-08-14 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA



Interesting. I think you're aware you can crash the demo by trying a sideways move after selecting the 1st node or going beyond the last.

About the visuals.
M is one of the bad looking letters in the pico-8 font due to the constrains of 3px fixed width. Using custom font or sprites will make it look much better.
The map seems to get more empty space to the right than to the left.
Since you're already handling some horizontal shifts, when one node touches the screen border and the one on the other extremity doesn't, the gap between the not touching one and the border could be split between all nodes of the line.
Dark blue and black are very close visually, and it can break the round feel of blue circles depending on where the 1st black pixel of the link lands.
Plenty if ways to fix that : dotted lines, starting the line one pixel diagonally from a blue pixel, gradient light to dark lines...
Code wise,I wish the "show code" of BBS was fixed. I'll check later and com back if I have something relevant to say.
You could always have a cart for the map and another one for the game, and have the carts call each-other with the game state info as parameter.


Thanks for your comments:

Crashing - yes, I'm aware. I fixed the sideways crash last night locally, need to fix the vertical one
Visually - yes, it's terrible at the moment, will likely switch to sprites
The map seems to get more empty space to the right than to the left. - I hadn't noticed that but will have a look. Not sure why that would be. All paths should be random and only the first 2 have to be different from each other.
the gap between the not touching one and the border could be split between all nodes of the line. - will think on this

In my game I will just use the map as an include, the map is a metatable and will be initiated on a change of game state. (I will shift all the user input handling onto the map)

In terms of the code, I'm not super happy with the method I'm using for storing paths. (the grid is much easier) I find it hard to visualize what the data looks like and the code to work through those paths feels very complex. Any suggestions on that would be welcome.


Took a peek at the code.
There seem to be a typo in the probabilities (0.5 instead of 0.05) but since it's the last meaningful value that's supposed to add to 1, getting overboard won't change the behavior, as long as you don't use the last impossible event. In fact, getting significantly overboard is a way to avoid problems of addition rounding, since some of the probabilities can't perfectly be represented in binary.

data structure seem tricky to deal with.
Instead, you could use a 16 lines 7 colums 2D array of event objects.
NIL for empty events, only integer indices.
for the links, since it's always 1 floor up, just a simple next_up array of 7 booleans would do.
When you need to check inbound connections of event L,C, just iterate on the line below and check if MAP[L+1][I].next_up[C]

I'm not so sure any more about tree unbalance. It's probably just a visual feel due to the entire map being left aligned. Shifting the whole map a few pixels to the right is likely to fix that.


Thanks again, for the next up , given a node on the grid could have multiple paths wouldn’t I have to flip any falses to true whenever I iterate through a grid position that already has an event? That way all paths could be represented in one array.

Apologies if I’m missing something obvious.


Here's a visual representation of the next_up (7 booleans, true/green for link) I'm suggesting to put in each event of the 7 x 16 grid.

Hope this makes more sense.
maybe link_up is a better variable name.


thanks again, not really following the visualization but I've made something that works and simplifies things.

Let's call it next_up in my description below but it's probably best to call it incoming_paths since the grid is actually upside down (and I render it the other way up)

When I initialise the grid I set every node's next_up to {false, false, false, false, false, false, false). This means I only need to flip to true later on.

I have a function that iterates through each path and randomly places a node on the grid for that path. (nothing new here) I keep track of previous x that I placed for the previous y.
In this function I set next_up for the current x to be true at the position of the prev_x

e.g. self.grid[y][x].next_up[prev_x] = true

If another path comes through this x in a later iteration, I build up a picture of all incoming node positions for this node e.g. {false, true, true, false, false, false, false} tells me there are incoming paths at 2,3

Now I have a separate function that iterates through the completed grid and generates events (to avoid any confusion with setting the node's event multiple times for each path)

My is_valid_event function iterates through the next_up for the current y, x and if the next_up is true looks up the event at that position to see if it is valid or not

This approach is an awful lot cleaner than using path_positions. I just need to update my rendering to draw the paths from next_up and the other bits of code that used path_positions.

function Map:connect_rooms()
    local starting_rooms = {}
    self.path_positions = {} -- Keep track of the positions visited by each path
    printh("-------------------------------------->>>>>>>")
    for path=1, self.PATHS_COUNT do
        local current_x
        local prev_x
        -- path 1 and 2 MUST start in different positions
        if path == 1 then 
            current_x = flr(rnd(self.GRID_WIDTH)) + 1
            add(starting_rooms, current_x)
        elseif path == 2 then
            repeat
                current_x = flr(rnd(self.GRID_WIDTH)) + 1
            until current_x != starting_rooms[1]
        else

        -- any other path can start in any position
        -- even if that means occupying the same node as path 1 and/or 2
        -- this will give us an affect of branching paths
        current_x = flr(rnd(self.GRID_WIDTH)) + 1

        end

        -- the first row in the grid is always combat
        self.grid[1][current_x].path = path 
        self.grid[1][current_x].event = "m"
        --self.grid[1][current_x].next_up = {[current_x] = true}

        -- we use path positions later to ensure path's don't cross
        self.path_positions[path] = {[1] = current_x}
        prev_x = current_x
        prev_y = 1

        -- Loop through each floor up the last floor before the boss room
        for y=2, self.GRID_HEIGHT-1 do
            -- Determine the range of possible next rooms (within bounds)
            -- possible next rooms are to the left, in front and to the right one floor ahead
            local next_x_start = max(1, current_x - 1)
            local next_x_end = min(self.GRID_WIDTH, current_x + 1)

            -- Choose a random room on the next floor within the range
            local next_x
            local valid_move = false
            local attempts = 0
            repeat
                next_x = flr(rnd(next_x_end - next_x_start + 1)) + next_x_start
                valid_move = not self:is_crossing_paths(next_x, y, current_x)
                attempts += 1
                if attempts > 10 then
                    -- If we can't find a valid move after 10 attempts, stay in place
                    next_x = current_x
                    valid_move = true
                end
            until valid_move

            self.path_positions[path][y] = next_x -- Add the new position to the path positions
            self.grid[y][next_x].next_up[prev_x] = true
            self.grid[y][next_x].path = path

            printh("Path: "..path.." Current: "..y..":"..next_x.." Previous"..prev_y..":"..prev_x)

            -- Update current_x to the chosen room for the next iteration
            prev_x = next_x
            prev_y = y
            current_x = next_x

        end
    end

end

function Map:place_events()

    for y=2, self.GRID_HEIGHT-1 do
        for x=1, self.GRID_WIDTH do
            if self.rest_positions[y] then
                self.grid[y][x].event = "r"
            else
                if y < self.GRID_HEIGHT-1 then
                    self.grid[y][x].event = self:generate_event(y, x, y)
                    self.grid[y][x].jitter_x = 2+flr(rnd(5))
                    self.grid[y][x].jitter_y = 2+flr(rnd(5))
                end
            end
        end

    end
    self.grid[16][4].path = 3
    self.grid[16][4].event = "b"
    for path, positions in pairs(self.path_positions) do
        self.path_positions[path][16] = 4
    end

end

function Map:is_valid_event(event, floor, x, y)
    -- self.path_positions[path][y] = 4

    -- Rule 1, no rests or elites allowed before 6th floor
    if floor < 6 and (event == "e" or event == "r") then
        return false
    end

    -- Rule2, no rests allowed on the 14th floor
    if floor == 14 and event == "r" then
        return false
    end

    -- Rule 3, no consecutive elite, shops, or rests
    -- we need to check all incoming paths to satisfy
    -- this rule

    local restricted_events = {e = true, s = true, r = true}
    self.invalid_path_count = 0
    if restricted_events[event] then
        printh("attempting restricted event "..event.." at "..y..":"..x)
        local next_up = self.grid[y][x].next_up
        printh(self:get_next_up(next_up))
        for i=1, 7 do
            printh(self.grid[y-1][i].event)
            if next_up[i] and restricted_events[self.grid[y-1][i].event] then
                self.invalid_path_count +=1
            end 
        end
        if self.invalid_path_count > 0 then return false end

    end

    -- Rule 4: A Room with 2 or more Paths going out must have all destinations 
    -- event types be unique
    -- @TODO: need to test this

    local outgoing_paths = 0
    local outgoing_events = {}
    for path, positions in pairs(self.path_positions) do
        if positions[y] == x and positions[y+1] then
            outgoing_paths = outgoing_paths + 1
            local next_x = positions[y+1]
            if self.grid[y+1][next_x] and self.grid[y+1][next_x].event then
                outgoing_events[self.grid[y+1][next_x].event] = true
            end
        end
    end
    if outgoing_paths >= 2 and outgoing_events[event] then
        return false
    end

    return true
end

The next_up I was suggesting is exactly the same thing as your outgoing_events.
If you don't want to use the sprite sheet or bother with a custon font, you could have an array of 1off characters strings.

-- in init
graghics['M']="\^:0000111B1F151111"
...

--in map draw
print(graphics[letter])

Thanks again for your help. Took a while but I was able to use the set of booleans to accomplish five tasks:

  1. restrict the user to only moving up nodes from the current path in the UI
  2. preventing paths from crossing over each other
  3. drawing lines between paths
  4. preventing consecutive events on the same type
  5. Making sure that a node with 2 or more Paths going out must have all unique event destinations

I've been able to remove the head wreck of the path_positions table



[Please log in to post a comment]