Each summer the r/roguelikedev[1] community has a summer event[2] in which we all make a simple roguelike, roughly following the Python libtcod roguelike tutorial[3]. Last year I tried to clone Dwarf Fortress in 40 hours. That was too ambitious. But I did enjoy working on a “fortress mode” project more than an “adventure mode” project, so I’d like to do something like that this year, but with a smaller scope. Requires keyboard+mouse.
Icons from game-icons.net[4], CC BY 3.0. Code repository on github[5].
Unlike a typical “fortress mode” game, I wanted to have an end to the game. This was inspired by Against the Storm[6], which lets you build a town but then you win or lose, and you start another town.
In my game the wilderness is all accessible at the beginning. You can use natural resources at small scales. But to advance, you need to set up rooms to process resources. Last year, building rooms and walls tile by tile is what doomed my project. This year, I generated all the rooms procedurally. You’ll claim and populate them to progress.
Skip ahead to the conclusion.
0 Setting up#
The r/roguelikedev summer event follows a Python tutorial for an “adventure mode” game. I’m going to adapt the tutorial topics for a “fortress mode” colony simulator game.
Last year I said I feel like a JavaScript project is easier to set up than a Python project, and I continue to feel that way. I did use a build step last year, but it’s not necessary to do that, and most of my projects don’t use a build step. This year I’m going to start out without a build step, and only add one if I need to.
1 Moving around#
Last year I was trying to make a Factorio-like interface where you walk around with a character using your keyboard, even though you’re building a base. I decided that it didn’t work well. So this year I’m going to try a more conventional mouse-based map scrolling interface. Left mouse drag will scroll the map.
I used the drag code from my draggable object guide to move the camera position. But how do I calculate the view from that? The view is a rectangle centered on the camera position.
The top left of that rectangle is the offset for drawing to the screen. To find the top left, I take the camera position and subtract half the view box size. Then I take the world coordinates of whatever I’m drawing and subtract the offset to the top left corner. If the object is at (50, 35) in the world, and the top left corner of the view box is at (50, 35), then I want to draw the object at (0, 0) on the screen.
The second thing I wanted to do here was to constrain the camera position to places where the map would be in view. Think about where the view box can be, and then where the camera position can be:
The camera’s position is restricted to a rectangle the size of the map, but shrunk by a half view box at each corner. This will keep the view inside the map region. A little detail: I added a slight margin to this calculation so that you can go outside the map region just a tiny bit. This adds a visual border to show that you’re at the edge of the map.
For the other controls, I want to try making something trackpad friendly, requiring only the left button, but then using keys to modify the mouse action. For example, instead of clicking through a menu to select “storage area” and then going into a “storage area marking mode”, I might try “hold down S while dragging a rectangle”. This wouldn’t require navigating menus. The mode is activated while you hold down the key and the rectangle ends when you release the key. I don’t know whether I’ll like this until I try it.
Spoiler: I liked it a lot, but holding down a key makes the cursor disappear on Firefox/Mac.
2 Entity, render, map#
I did Part 3 (generating a map) first, and then came back to Part 2 (rendering that map, and entities).
Last year, pathfinding turned out to be tricky. Colonists could be building a wall that blocks another colonist’s movement. To reduce the scope of the game, I’m going to try simplifying the pathfinding requirements.
- The player won’t dig out rooms or build walls. The rooms already exist in the map, and are unlocked.
- Pathfinding from room to room can be cheap and precalculated. These paths funnel through the doors.
- Pathfinding within a room is still tricky, because I want the player to build objects within the room, and those objects would block paths. I need to prohibit building an object that blocks paths across the room. Or … I can let colonists walk over anything, but assign a higher movement cost to them so that they prefer to walk in open areas.
- Colonists can pass through each other. This simplifies the pathfinding obstacle management. The only obstacles are static objects within rooms.
So I implemented pathfinding, using breadth first search for now, but can switch to A* later. I put 25 colonists into the world and told them to find paths to random locations, then walk there. When they have finished walking, they find another path. This tests the pathfinding system before I have jobs for the colonists.
A side note: Python is so wonderful in how it allows tuples to be used in so many places, including keys for dicts. In JavaScript, an Object can have string keys, and a Map can have object keys, but there’s nothing convenient like Python’s tuples for storing (x, y) pairs. C++ and Rust have the convenient tuples too. I miss this when working in JavaScript, and hope that one day they get added[7], but there seems to be no progress on that proposal.
3 Generating a map#
I liked the left/right nature of the map last year, and I want to do that again. I’ll put the wilderness on the left and the dungeon rooms on the right. I reserved the left side of the map for water, then wilderness. Then the rooms begin.
The dungeon rooms will come from the offgrid algorithm that Chris Cox sent me. It’s only 20 lines of code. Last year I used thin walls. I think they were neat, especially helping with player-built rooms. I don’t have player-built rooms this year. I’ll go back to thick walls. Using the offgrid algorithm, I’ll generate left
, right
, top
, bottom
for each room. These are half-open intervals, so left <= x < right and top <= y < bottom. But I also need to reserve one tile for the thick walls, so I’ll use left < x < right and top < y < bottom.
The next step was to place the walls. There are two parts to this:
- All tiles within a room are walkable.
- All tiles to the left of the leftmost room are walkable. This handles the wilderness areas.
- If there’s no leftmost room on a row, use a default border between wilderness and mountain.
The next step after that is doors. I had previously written a door algorithm that looks at all tiles that have two adjacent room tiles and two adjacent wall tiles. That algorithm would work here. But I can use something similar. Since my rooms are generated from a grid (using the offgrid algorithm), I can use that original grid to find adjacent rooms. For each room, I pick a random tile on the west wall for a door, and I pick a random tile on the north wall for a door, unless the room is in the topmost row. This produces plenty of connectivity, perhaps too much for a traditional adventure mode dungeon, but probably just fine for a colony simulator:
This produces a way to go from the wilderness into the dungeon, and a way to go from dungeon rooms to adjacent rooms.
4 Field of view#
The original tutorial calculates which tiles are visible to the player. For this colonist style game I want to compute which rooms and doors are visible to the player. Last year I made everything visible, so I skipped this part. This year, I want the player to unlock rooms as you go through the game, so those rooms won’t be visible until you open them up. Here are the visibility rules I came up with:
- The player can view the wilderness.
- The player can view unlocked (colonized) rooms.
- The player can view the outline but not the interior of unlockable rooms.
- The player cannot view locked rooms that aren’t currently unlockable.
- Unlockable rooms are currently locked and connected to an unlocked room.
- There can be at most 3 unlockable rooms at a time. I’m hoping to get a “pick one of these 3 cards” vibe here.
- Once a room is unlocked, it stays unlocked.
- A door is visible if either of the rooms it is connected to are visible.
Separate from room lock/unlock status, I’m maintaining a walkable set of tiles. I decided the easiest thing to do was:
- The walkable set starts out as the wilderness area, and doors into the wilderness area.
- Unlocking a room adds the room’s tiles and doors to walkable.
At some point, unlocking rooms will require resources. For now, I will make it free. I need a UI to unlock rooms. Part 7 in the original tutorial was about interface, so I’m going to work on that now.
5 Interface (Part 7)#
This was the topic of Part 7 in the original tutorial. I think UI needs to come earlier in a management game like this.
I spent a lot of time last year looking at other games. One of the biggest problems I see in colony games is that it’s hard to understand what’s going on and what to do to fix problems. The interface is a big part of that. I am hoping that the focus on room-by-room building rather than tile-by-tile building will let me make the information display less granular. I might also be able to make some decisions more explicit. For example, instead of “workshop output is taken to the nearest stockpile” as a tile by tile decision, I could ask the player to designated a room’s output as being another room. Then the information display can be a giant arrow from one room to another.
I’m going to try a UI where you hold down a modifier key to change what the mouse does. Hold down R to activate room mode, and then click on a room to unlock it. I need to have a mouse click handler that checks for clickable rooms. And I need to have the mouse pointer change when moving over a clickable object. The click handler should take precedence over map dragging.
I decided I would have a UI mode that controls how to render and how to interpret mouse events. The key state controls which UI mode I’m in. However, as usual, this simple model didn’t quite work cleanly. There were things I needed to deal with like: when pressing R, I want to highlight the room under the mouse pointer, but the keydown
event doesn’t include the mouse position. That means I need to be storing the mouse position always, even before I need it. Another case is that when pressing R then Shift then releasing R, the keydown
event tells me 'r'
was pressed but the keyup
event tells me 'R'
was released. There are some edge cases I’m just not going to deal with for this project, but I want to keep it in mind for future projects.
Not all rooms are unlockable. Rooms must be connected to an already unlocked room. The way my data structures are written, the easiest way to calculate this was to look at all the doors. A door connecting an unlocked room to a locked room means that locked room is unlockable.
I also want rooms to have labels. I think in a management game it’s important to see the “big picture”, and I think labels can help. I need to size the labels so that they fit. I used the TextMetrics[8] feature to resize them:
The room labels will be “Kitchen”, “Dining”, “Bedroom”, etc. but I haven’t implemented room types yet.
Bug: I noticed that on Firefox/Mac, holding down a key causes the cursor to mostly disappear. Weird. This is not only on my site, but on any site. It seems fine on Firefox/Linux and on Chrome.
6 Placing enemies (Part 5)#
Instead of enemies, I have lots of friendly colonist NPCs. They need to have motivations, jobs, inventory, pathfinding, etc.
I want to place furniture objects within rooms. These objects will correspond to the use of the room. For example, a bedroom’s furniture will be beds.
Should I make colonists build the furniture, or should they be built immediately? I think immediate is easier to implement, but building furniture is a great activity for colonists. Given how this project is too ambitious already, I will do the simpler thing.
I had been imagining each furniture would have one tile per input, and one tile per output. The room would have an input storage zone and an output storage zone. Colonists would move items from the input zone to the furniture, and when the furniture has all its inputs filled, a colonist can do that job, filling the furniture’s output zone. Then there’d be a job to move items from the furniture to the room’s output zone. But I wrote all of this out on paper and decided it needed to be simpler.
So the next plan was for each furniture to have one tile per input, one tile to stand on, and one tile for the sprite. I dropped the furniture’s output; I can reserve a spot in the room’s output zone (but this means I need to implement a tile reservation system, so it’s not clear this is actually simpler). I also dropped the room’s input zone. Colonists can take items directly to the furniture’s input zone instead of first buffering them in the room. These are imagined simplifications. I don’t know if it will make the job system more complex. But I’ll try it and see.
I also considered simplifying further by having furniture be a single tile, with no tile to stand on and no input tiles. But I imagined what that did to the job system, and I didn’t like what my imagination told me. So I’m going to try implementing multi-tile furniture (one tile for the sprite and several tiles reserved for items or colonists).
Possible room types and their object types:
room | object | inputs | outputs |
---|---|---|---|
wilderness* | berry | - | food |
wilderness* | tree | - | wood |
underground farm | - | - | crops |
mushroom cavern | - | - | crops |
kitchen | cooking table | raw food | meal |
dining | dining table | meal | - |
bedroom | bed | - | - |
iron mine | mining pit | - | iron |
quarry | mining pit | - | stone |
crafting | crafting table | wood, stone, iron | crafts |
I don’t know what the “end game” of the production tree will be, but I think crafts could be the cost of opening up later rooms.
I’d also like to have some room unlocks carry bonuses or penalties, like “workahol fountain — your colonists no longer sleep” or “teleporter accident — you double the number of colonists but they work half as hard”.
6.1 Multi-tile objects#
I haven’t worked with multi-tile objects before. These will be furniture, not moving objects, so it’s simpler. To place the furniture, I need to figure out whether all of its positions are unoccupied. A helper function can calculate all the positions that the furniture uses up (stand position, input positions, and sprite positions). Then, for each position in the set:
- check that it’s in the room bounds
- check that the room is unlocked
- for each existing furniture in the same room, check that it’s not already occupied by that furniture
I don’t have to check against furniture in other rooms, because I’ve already ensured the new furniture entirely fits into the current room.
I had an accidental information leak: the hover message revealed the room type, but if the room was locked, the player shouldn’t know the room type yet. I fixed this by only showing the information for unlocked rooms.
6.2 Pathfinding#
I already implemented pathfinding earlier, using the walkable
map, but now I have furniture. If I make furniture block paths then I also need to make sure that the player can’t place furniture that blocks access to the room. So I decided instead that furniture would slow but not block the colonists.
This means I need to switch from the unweighted pathfinder (breadth first search) to a weighted pathfinder (like Dijsktra’s). There are only two weights though, 1 for an unoccupied tile and 4 for a tile containing furniture or item. This means the implementation can be a little simpler than a full Dijkstra’s Algorithm. Instead of a sorted frontier of tiles, I can have 5 unsorted sets of tiles, one for each weight relative to the lowest weight (0, 1, 2, 3, 4). However, the tie-breaking hack[9] that made paths look better doesn’t work when adding movement costs. Instead, I need a different tie-breaking hack[10], which means I have more than 5 unsorted sets. I think the BFS tie-breaking might work, but … I should implement something now, and optimize later.
Separately, I’d like to make the movement match. I can do that by repeating the slow tiles in the movement path array, so for example if tile A
is fast and tile B
is slow then the stored path can be [A, B, B, B, B]
so that the colonist would take 4 ticks on tile B
. But I think this is a low priority for me. The main goal is to make the paths avoid the slow tiles. If they avoid slow tiles, then it doesn’t matter if the slow tiles are actually slow. So I’m going to skip this for now.
To implement pathfinding, I need to quickly look up whether a tile is occupied, so that I can determine the movement cost. I could build a global occupied
map from position to furniture|item, but I decided instead to make a per-room occupied
map.
- I changed
walkable
from being a set of positions to being a map from positions toRoom | Door
. But because Javascript doesn’t have tuple values, the actual type isMap<string, {pos: Pos, in: Room | Door}>
. - I added
room.occupied
as typeMap<string, Furniture | Item | null>
.
To determine what’s on a tile, I’ll first look up the room in the walkable
map and then look in the room.occupied
map. I don’t know that this is the best choice. But I also realize that sometimes I spend too much time trying to pick the best choice, when all I really need is a reasonable choice.
[Spoiler: I never did switch to Dijkstra’s]
7 Combat (Part 6)#
I had considered having a simplified combat as part of the cost of opening up a new room. You’d have to decide how many colonists to send into a new room to defeat the monsters. Send too few, and you might not get the room. What is the downside of sending too many? Maybe they have to rest for a time afterwards, so that reduces your productivity. Is this at all an interesting/fun decision to make? Not sure.
Another option would be to go the Dungeon Keeper or Tower Defense route, where you have to deal with enemies trickling in. As much as I’d love to make a Dungeon Keeper style game, I think I don’t even know how to do the basics so that’s something to tackle in a future project.
In the interests of simplicity, I’ve decided not to have any combat. I can come back and add it later if I have time. But I am pretty sure I won’t have extra time.
8 Items and inventory (Part 8)#
Items are generated by resource objects, stored in stockpiles, and transformed by workshop objects.
A transport job asks a colonist to take an item from one place to another. But why? Each room needs to generate “demand” for some items, and also “supply” from production. Some rooms like mushroom farms will generate supply over time. Other rooms like crafting will generate supply only when a colonist is working there. Maybe I can unify these two types somehow.
A production job asks a colonists to go to a room and transform some input to some output. The input or output might be empty — for example, in the dining room, the input is food and the output is empty. But that should affect the colonist’s hunger level, so I need to track that somewhere and also make it visible somehow. And the colonist shouldn’t be asked to do the job unless their internal hunger level is low. Hm. So this means colonists need internal stats.
For simplicity, only furniture can generate jobs. Nothing else generates jobs. For example, a colonist being hungry does not generate a job to go get food. Instead, the dining table generates a job to bring food to it, and also generates a job to have a hungry colonist eat there.
Let’s take an axe-making room as an example:
tool_shop: { furnitureShape: { name: "table", ticks: 60, stand: Pos(0, 1), inputs: [ {type: 'iron', pos: Pos(-1, 0)}, {type: 'wood', pos: Pos(0, -1)}, ], output: 'axe', sprites: [{type: 'table', pos: Pos(0, 0)}], }, }
The furniture in this room is a table, and it has some requirements:
iron
should be placed 1 west of the table (transport job)wood
should be placed 1 north of the table (transport job)
We’ll want a colonist to bring an iron and a wood to their respective tiles, then we’ll want a colonist to stand at the furniture for 60 ticks. That’ll consume the iron and wood, and produce an axe in the colonist’s inventory. The colonist will then drop it off somewhere in the room. I think the furniture’s state diagram would be something like this:
The colonist and item also have their own state transitions. This is going to be tricky to implement.
8.1 Item transitions#
Picking up and dropping off items are both used in several places so I will make helper functions for those state transitions.
I need to figure out how to store items. An item can be on the floor or carried by a colonist. That means its pos
is of type pos: Position | Colonist
. For pathfinding I also have an occupied
map from position to Furniture | Item | null
. The only way to create an item is with crafting, so the items will be placed into a colonist’s inventory. The only way to consume an item is with crafting, so the items will disappear from the input tile of furniture. So the transitions will be:
- Item created in a colonist’s inventory:
- ensure
colonist.inventory
isnull
- create an
item
(add new object to globalitems
list) - set
item.pos
tocolonist
- set
colonist.inventory
toitem
- ensure
- Item is consumed on the ground:
- ensure
item.pos
is on the ground - set
occupied[item.pos]
to =null=¹ - set
item.pos
tonull
- destroy
item
(remove from globalitems
list)
- ensure
- Colonist picks up item:
- ensure
item.pos
is equal tocolonist.pos
- ensure
colonist.inventory
isnull
- set
occupied[item.pos]
to null¹ - set
item.pos
tocolonist
- set
colonist.inventory
toitem
- ensure
- Colonist drops item:
- ensure
occupied[colonist.pos]
isnull
- ensure
item.pos
iscolonist
- set
occupied[newpos]
to =item=¹ - set
item.pos
tocolonist.pos
- set
colonist.inventory
tonull
- ensure
I like to write down these transitions before coding them. I want to make sure that related data structures are kept in sync.
¹ I ended up not implementing an explicit occupied[]
map. I realized that I need to get the basic game working first before worrying about efficiency, and the occupied[]
map is there for efficiency only.
8.2 Job candidates#
If any of the input requirements aren’t met (tile
is empty), then we’ll create a transport job for each of them.
- An item needs to be available somewhere (with the correct
tile.type
, andtile.job
is null), and a colonist needs to be available (colonist.job
set to null, andcolonist.inventory
is null). Create aTransportJob
object withcolonist
anditem
set. Setcolonist.job
,item.job
,tile.job
to point to this new job object. Set the colonist to find a path toitem.pos
. Setjob.phase
to pickup. - The colonist has reached the
item
, and should pick it up. Setitem.pos
tocolonist
. Setcolonist.inventory
toitem
. Set the colonist to find a path totile
. Setjob.phase
to dropoff. - The colonist has reached the
tile
, and should drop theitem
. Setitem.pos
totile
. Setcolonist.inventory
to null. Setcolonist.job
,item.job
,tile.job
to null. Setjob.phase
to done. Remove the job from the jobs list.
That’s a lot of error-prone code. A lot of objects are pointing to each other temporarily. I’d like to find a simpler representation. If this were SQL I’d create a jobs
table with (phase, colonist, item, tile)
with an index on all the fields. Maybe that’s the way to go here, even without the index. Hm. Something to ponder.
If all the inputs are filled, the furniture creates a production job.
- The furniture needs to be unused (
stand.job
is null), a colonist needs to be available (colonist.job
is null,colonist.inventory
is null), and some output tile in the room needs to be open (neither reserved nor occupied). Some jobs may require a colonist to have some status (likecolonist.hungry
). Create aProductionJob
. Setstand.job
,colonist.job
, anddest.job
to this job. Set the colonist to find a path tostand
. Set the job phase to move. - The colonist has reached the
stand
position, and should start working. Setjob.timeCompleted
to the current time plusticks
. Set the job phase to work. - The current time is past
job.timeCompleted
. Destroy the input objects. If there’s an output, create an output object, and put it intocolonist.inventory
. Set the item’s.pos
to the colonist. Set the colonist to find a path todest
. Setstand.job
to null (this frees up the furniture to start another job). Set the job phase to deliver. - The colonist has reached the delivery point, and should drop the item. Set
item.pos
todest
. Setcolonist.inventory
to null. Setcolonist.job
anddest.job
to null. If the job required the colonist to have some status, clear that status. Set the job phase to done. Remove the job from the jobs list. - If there is no output for this job, wander randomly so that the colonist is not standing in the way of another job.
Again, there’s a lot of complexity here, and it’s setting off my “error prone! error prone!” alarm bells.
There are some invariants I should write down, such as:
colonist.inventory
can only be non-null whilecolonist.job
is non-nullcolonist.path
can only be non-empty whilecolonist.job
is non-null (update: this turns out not to be true!)
8.3 Data structures & ECS#
There are a whole lot of state transitions that I want to get right. One way to choose a data structure is to list the desired operations. So I made a list:
- Find an available item with a given
.type
and that’s not being used (.job
is null). Probably better to pick a nearby item. - Find an available colonist that’s free (
.job
is null), not holding anything (.inventory
is null, but this is implied by having no active job), and optionally having some status (like hungry). - Find an available tile in a given room that’s free (
.job
is null, no item placed there). - Find all currently active Production jobs in the work state that have their
timeCompleted
past expiration. - Find all furniture that’s not currently used (
.stand.job
is null). - Find all furniture that has some input not filled (no item on that input tile).
Is that it? Since the number of items of a given type, the number of colonists, and the number of tiles in a room are not too many, maybe the simplest thing is to search an array. If it turns out to be a problem I can build an index later.
In Object Oriented Programming we might model the connection this way, with objects pointing to each other:
In the Relational programming model[11], we would store the connections outside the objects, in a table:
In the Object Oriented model, when you have the object, you can look up the fields. For example, if you have a colonist
you can find out the colonist.job
. But if you had the job
you can’t look up which colonist has it. That’s why we have to store the redundant job.colonist
field. And it’s up to us to make sure the bidirectional link is maintained. In the Relational model, you can look up in any direction. You can ask the Jobs table which colonist has item I1, or you can ask the same Jobs table which item does colonist C1 carry. There’s no redundancy so the data is simpler to keep correct.
Note that the Entity-Component-System (ECS) model is a subset of the Relational model, typically with one primary table for entities that links with one side table per component. But I’m not trying to set up an ECS here.
Since my priority here is to reduce errors, I’m going to try using the Relational model here. For performance, the tables can have indices for each column we want to use for lookup. I can add that later if I need to.
TL;DR Instead of maintaining bothx.y = y and y.x = x I will simplify by storing only one of them and then doing a search to find the other direction.
8.4 Bugs#
I implemented the Transport and Production job system, and tested it with
- furniture with 0 inputs and 1 output
- furniture with 1 input and 0 outputs
- furniture with 1 input and 1 output
I ran into several tricky bugs. I think the table data structure helped a lot, but I still had bugs with the state transitions and with invariants.
- colonists would pick up an item from an input tile and move it to another input tile, endlessly; the fix was when searching for an item, don’t pick up anything from an input tile
- colonists would stand at furniture after doing a job if there’s no output item, blocking other colonists from using it; the fix was to path to a random location
- colonists with no job would have a path, which violated an invariant; this resulted from the previous fix; the fix was to drop that invariant
- colonists would drop output items on furniture; the fix was when searching for a suitable location, skip furniture tiles and their input tiles too
- while a colonist was moving to a tile to drop off an item, the player could build furniture there; the fix was to block building on items and also on tiles reserved for future items
I probably should have made the state machine explicit. I represented the state implicitly, based on whether path
and inventory
and other fields were set. An explicit state machine would’ve been redundant, I thought. But redundancy is often useful for verifying assumptions and finding bugs.
I ran into a “design bug”. There’s no particular reason not to sleep, so a colonist might sleep, then wake up and go to sleep again. To avoid this I assigned “status effects”. A job could clear the colonist’s status effect, so the colonist would only take the sleep job if they have the sleepy
status effect. Then once they’re finished sleeping it clears the flag, and the colonist wouldn’t immediately go back to sleep.
I ran into another “design bug”. The colonists are assigned any job that’s available. But if they’re sleepy they might just happen to be assigned a cooking job, and maybe they’re assigned cooking jobs all night. I wanted to prioritize basic needs like sleeping and eating, so I added a priority value per job type. Eating is the highest, then sleeping, and only if the colonist doesn’t have a job that satisfies those will they consider cooking, farming, etc.
9 Ranged scrolls and targeting (Part 9)#
I don’t have ranged scrolls but I expect to have multiple types of building UIs. In particular, I need to add a UI for rectangular zones. My initial thought was that you will press the key on the top left corner, move the mouse, release the key. But a more conventional UI would be to use the mouse button to mark a rectangle.
10 Saving and loading (Part 10)#
I’m not going to attempt this. I think it’s great to have, but it’s not important to me right now.
11 Dungeon levels (Part 11)#
In an adventure style game, each dungeon level is standalone, and they increase in difficulty as you progress. In Dwarf Fortress, the levels aren’t standalone, but they do increase in difficulty as you dig deeper. But in this game I don’t have dungeon levels. Instead, each room could be thought of as a miniature level to conquer.
12 Increasing difficulty (Part 12)#
In a normal adventure mode roguelike, difficulty increases as you go down into the dungeon. In this game, I’d like to have the difficulty increase as you go right (east) into the mountain.
13 Gearing up (Part 13)#
As much as it might be fun to have gear for the colonists, I am going to skip this. I need to keep the scope down.
14 Conclusion#
I made it much farther this year than last year. I kept the scope as small as I could while still having the core idea of colonists moving around and doing jobs.
I like the interface I came up with. The R and F keys act as modifiers to change the data view and also change what clicks do.
This was my first chance to use the offgrid algorithm for map generation, and it went very well. Offgrid is the simplest thing to use for dungeon map generation!
In my planning I had too much premature optimization. For example in the pathfinding section I planned to have a hierarchical pathfinder that precalculated room-to-room paths. That was complete overkill. I didn’t even end up implementing A*. I also had planned to build spatial indices but didn’t need them either.
I’m very happy with how the furniture + job system worked out. I was able to find something that was simpler and worked better than my original plan. The data structure was nice, and I will blog about it. There were lots of bugs though and if I work on something like this again, I’d like to find a way to write the code in a less bug-prone way.
Job assignment in general is tricky. Goblin Camp uses the O(N³) Munkres-Kuhn Algorithm. Factorio recently posted about issues with their robot job algorithm[12] and what they’re doing to improve them. I’m using a simple greedy algorithm here.
My job system works “forwards”. Jobs for raw materials run, then trigger jobs for intermediate products, then trigger jobs for final products. A game like Transport Fever has a “backwards” job system. Jobs for final products run first, creating demand for intermediate products. The jobs for intemediate products then run, creating demand for raw materials. It might be interesting to experiment with this.
Having all the data displayed on the page was useful for debugging. Also, assigning each object a unique id was useful for the debugging views. I should’ve put it there at the beginning.
The biggest thing missing is that this isn’t much of a game. I have the core mechanics (colonists, jobs, furniture, room unlocking) implemented but there’s no overall flow. There’s no reason to unlock new rooms. There’s no cost of unlocking rooms. There’s no reason for colonists to eat or sleep. There’s no reason for anything to happen.
Despite not having built a full game, I’m happy with how the project went. I kept the scope down. I learned a lot. I have some ideas to use in future projects. And maybe next year I’ll attempt to make this into a game.