Revengate development log
Weekly progress reports about the Revengate development.
Website | Git repo | Google Play
2023-11-18
I started working on saving games. Godot custom resources are somewhat similar to Python pickles: they can both save arbitrary native and user-defined types, and they both suffer from arbitrary code execution vulnerabilities. For Revengate, the latter not a big deal because it's hard enough to upload files inside the Android sandbox that no player is ever going to try using saved games that they got from shady places on the internet, and even if they did, the malicious code would not be able to do anything outside the sandbox since Revengate requests no Androids permissions at all. As long as we don't have a desktop version of the game, that's a productivity shortcut that I'm very comfortable taking.
Another big pro of custom resources is that new fields can have a default value, which provides some level of forward compatibility. It's not perfect, but it covers many cases. For the rest, I can only offer the player to delete their old saved games because Android won't let them downgrade the game even though I publish all the old APKs (well, it's doable, but it's really hard).
Custom resources work amazing for flat data, but you need an extra twist to make them deal with data trees. Thankfully, Godot also has PackedScenes
for that. Basically, that's how the editor saves your scenes as .tscn
files. And that is also where the similarity with Python pickles end because there is a fair bit of magic with how Godot decides what nodes should be part of a packed scene. Thankfully, I found this very helpful diagram. Basically, the owner
of a node has to be inside the PackedScene or has to be in its parent chain. Fair enough, but Godot will also void the owner field for various reasons, including when you reparent nodes, which is how the hero moves to between levels and how actors pick up items in Revengate. So long story short, I think that the best way to do it is to recursively set the owner to the root node of what I'm saving just before the actual save. That seems too work, but I'm aware that I might be forgetting about a few edge cases. And the beauty here is that everything is included: poisoning conditions, bot goals, memory of who offended whom... Everything!
In any case, I'm way further on this than I thought I would be after one week, which is really good for my motivation!
Next: figure out how to tell the rest of the game about a restored state. That is, reconnect all the UI signals and refresh the view.
NO BLOCKERS.
2023-11-10
Swarming works! I tried to come up with a demo gif, but since most of the magic happens out of perception range, you'll have to take my word for it.
Swarming monsters seek each others. If they have enough connectedness, they might seek other actions, such as exploring the level. Only one in the swarm that is exploring is enough to move the whole swarm. If a higher priority action kicks in, such as kicking the hero's ass, the first the monster to see you will stop swarming, but the swarm will try to stay with it and soon the whole swarm will be after you. This is very different from, for example, giant bees in Nethack or spiders in Caves RL where they all start in the same room, but will readily diffuse after you wake them up.
JT paired with me on this one and I'm happy with how simple it turned out: if you can get closer to the peers that you can perceive, that's what you do, unless you feel adequately surrounded already. I was afraid it would end up as a jumbles mess that requires cross communication between actors, but we found a good way to reduce it to fully autonomous decisions.
I spent the rest of the week running Monte Carlo simulations and play testing until the polish and the balanced felt right. And since I got there yesterday, I pushed v0.11.0 on Google Play. It's properly git-tagged and should therefore appeared on F-Droid in a few days.
This release adds:
- daggers with a longer range;
- new building: church;
- new dungeon: crypt under the church;
- new weapon: MjΓΆlnir;
- new monsters: cherub, skeleton, sentry scarab, nochort, plasus rat, Algerian giant locust;
- new monster strategy: swarming;
- new item: carrot.
Bug that were fixed:
- can no longer chat across walls and closed doors;
- more consistent clearing of action highlight markers (the red and green squares around actors);
- monster strategies now obey perception (with select rare exceptions).
Player experience changes:
- single tap on distant actors pop a context menu: (get closer, inspect, ...);
- added a health bar;
- flash a message on with the reason why a travel command aborted (path block, under attack, ...);
- Quick Attack button is available as soon as you've attacked someone in range, even a friendly actor.
Next: items descriptions and saved games.
NO BLOCKERS!
2023-11-03
I paired with JT Wright who made a really cool Monte Carlo simulator for the furnishing deck builders. Revengate populates new game boards with monsters, items, and vibe by making a "card" deck of what could go there based on rules, what was previously generated, and budget. Then we draw cards until we meet our budget for the level (or the deck is empty).
The new simulator runs 1000 whole dungeons and then outputs a statistical summary of what has been drawn. For example:
Summary of depth 1
(c) desert centipede: 9.05% | p10: 0 p50: 0 p90: 1
(k) kobold: 29.09% | p10: 0 p50: 1 p90: 3
(l) labras: 15.17% | p10: 0 p50: 1 p90: 2
(r) rat: 11.24% | p10: 0 p50: 0 p90: 1
(s) sahwakoon: 11.65% | p10: 0 p50: 0 p90: 2
(o) sewer otter: 13.04% | p10: 0 p50: 1 p90: 2
(f) Sulant tiger: 10.77% | p10: 0 p50: 0 p90: 1
[...]
Summary of depth 6
(c) desert centipede: 11.38% | p10: 0 p50: 1 p90: 2
(π€) ghost: 9.62% | p10: 0 p50: 1 p90: 1
(k) kobold: 11.20% | p10: 0 p50: 1 p90: 2
(l) labras: 19.81% | p10: 0 p50: 1 p90: 2
(r) rat: 9.26% | p10: 0 p50: 0 p90: 1
(s) sahwakoon: 10.13% | p10: 0 p50: 1 p90: 2
(o) sewer otter: 15.30% | p10: 0 p50: 1 p90: 2
(f) Sulant tiger: 13.29% | p10: 0 p50: 1 p90: 2
[...]
Summary of depth 10
(c) desert centipede: 13.95% | p10: 0 p50: 1 p90: 2
(π€) ghost: 3.69% | p10: 0 p50: 0 p90: 1
(k) kobold: 4.51% | p10: 0 p50: 0 p90: 1
(l) labras: 23.33% | p10: 0 p50: 2 p90: 3
(P) pacherr: 3.22% | p10: 0 p50: 0 p90: 1
(r) rat: 8.46% | p10: 0 p50: 0 p90: 2
(s) sahwakoon: 9.36% | p10: 0 p50: 1 p90: 2
(o) sewer otter: 18.29% | p10: 0 p50: 1 p90: 3
(f) Sulant tiger: 15.19% | p10: 0 p50: 1 p90: 2
[...]
It reads as follow: kobolds represented 29.09% of the monsters generated at depth 1. In the 10% time with the fewer kobolds, there were none, the median number was 1 at this depth, and 10% of the times there were 3 or more.
As you go deeper, you start to see ghosts. Since ghosts have an extra rule that makes them very rare (just 3 per dungeons), you are less likely to see them at depth 10 since we don't have any ghost cards left at this point. This was not previously the case as the rarity of ghosts was entirely controlled by their high spawn cost, but the simulator allowed us to fine tune the generation with the extra card rule.
I added a few monsters and put then in the new deck for the crypt dungeon from last week: plasus rats, skeletons, and the scary cherubim. I used the deck simulator to make sure that the scary cherubim were adequately rare.
A few other monsters: nochorts, sentry scarabs, Algerian giant locusts.
I tried to make the giant locusts move as a swarm, but my naΓ―ve approach is not cutting it. The applicability of the strategy seems too rigid and the locusts fall out of swarming too easily. I want them to pass the control to other strategies for attacking and exploring, otherwise the swarming strategy needs to re-implement everything in the context of swarming. I really like the idea of swarm emergence, so I will experiment more on this one.
Next: maybe items descriptions, maybe I polish what I have and push a new release out. I'll see how I feel tomorrow morning.
NO BLOCKERS!
2023-10-27
The new crypt under the church is a series of long corridors with small chambers on each sides. This is strikingly more organized than traboules and more claustrophobic than the surface since it's so easy to have your escape path blocked off.
PreFabs can now emit cards that will go into the level furnishing deck (Actors, Items, Vibe). Cards can be either mandatory (you really want a priest in the new church) or probabilistic (might smell of incense, or maybe not). The spawn cards now have an optional spawn_rect
field to restrict where they can appear.
I helped Victor Pernet to add his game (Pirate Solitaire) on F-Droid. He got the whole thing to build clean, we are now waiting for the F-Droid team to audit his code to make sure that it is as Open Source as he claims. That will make Pirate Solitaire the second Godot-4 game on that app store.
Roguelike Celebration was awesome! The talks have not been posted online yet, so I will probably repost this when they are. My favourite ones were:
- Fireside chat about the development of NetHack by Jesse Collet, Keni
- Audible Geometry: Coordinate Systems as a Resource for Music Generation by Paul Hembree
- Backpack Hero - Player Upgrades and Progression by Jasper Cole
I also really enjoyed the unconferencing.
I tried to emulate the focus mode that I had near the Merced by removing the wifi password from my laptop. This way, I can only access the internet when tethering with one of my phones. When I want to focus, I put all the phones in a different room. It's too early claim any kind of victory, but I like how simple that was to implement.
Next: add the right vibe cards to the church, populate the crypt with undeads and scary angels.
NO BLOCKERS!
2023-10-20
I managed to do a few things while camping:
- added a health bar
- church prefab
- single-tap on a distant actor now pops a context menu, there is a new command to get closer (within action range) to them
- Traveling multi-turn command flashes a message on why it aborts (attacked, path blocked, ...)
- quick-attack button is available on friendly actors you've previously attacked
- carrots
- action highlight are properly cleared after a conversation, now using the
is_valid()
predicate of the Talk and Attack commands. - Healing monster strategy is now triggered by health-% rather than health points
- TribalTerritorial monster strategy is taking perception into account
magical
is now an Effect tag rather than a field
I felt really productive even though my coding was timebox to only a few late afternoons. It's weird, sitting there at a wobbly picnic table that was the wrong height, without my mechanical keyboard or my external monitor, code came easier than when I have a perfectly ergonomic desk setup. It took me a few attempts to get there. It seems like best coding spots were the ones where I didn't get any cell service. The calming sound of the a river helped too.
Yosemite is full of potential distractions: climb something, watch people struggle on the Generator Crack, refill my water jugs at Fern Spring, hang out with a beer in El Cap meadow while watching big wall climbers with binoculars. All those, however, have a fairly high activation cost, which puts then in big contrast with checking the news or going down a Wikipedia rabbit hole. I can obviously put my laptop in airplane mode when I decide to work on the game, but it's so trivial to override that commitment that I don't think it's going to work as well as sitting by the Merced river. It's also possible that the legend is true and that there is something magical about the Fern Spring water, so just to be safe I stocked up on several litters of the stuff.
2023-10-13
I made up my mind on what should go in v0.11 (you might have to scroll down, gitlab is weird). In short, it's mostly more work on UX but you still get a few new monsters, including the very scary cherub. I don't want to add new quests until it's possible to save and restore games, the cherub will therefore sit in a purely optional side-dungeon.
I upgraded my DialogueManager plugin for Godot. My speech bubble was doing integration based on the plugin internals rather than the doc, so I was afraid things would break. It did, but the public API now exposes all the signalling I need so it was well worth doing this cleanup.
2023-10-06
The threat of a government shutdown tricked me into changing my camping reservation in Yosemite. I lost a few good climbing days, but that gave me time to fix all (known) bugs in Revengate v0.10.0 and to push it to Google Play. It should also show up on F-Droid in a few days.
This includes the new quest and a few significant UX improvements.
2023-10-01
I made it possible to keep playing after failing a chapter. If you don't succeed a non-blocking quest, the quest NPC is a little pissed and you don't get a quest reward, but you can keep playing. This required decoupling the victory logic from the victory and game over screens. It was also the right opportunity to move "quests", which were mostly scattered conditions, as their own self-contained entities.
JT and I added a collection of surface areas to reach the new quest. This is pretty much a nice city stroll with lots of friendly NPCs and just a few rats and thieves. It's good to take a break from the non-stop action. We had fun writting the rumors and chatter you hear around town.
I'm heading out camping for a few weeks, so probably no more updates until after Roguelike Celebration. I was hoping to get the next build out before camping, but that won't happen.
2023-09-24
You can now offend someone from a conversation and someone who recalls being offended becomes hostile. This decays over time based on the severity of the offense. The new quest has variable rewards depending on if you stick to beating up the informant or lose control and kill them. Better go with a weak weapon if you happen to hit pretty hard.
There is a new monster: the yarohu.
Worked with JT on balancing the new monsters and NPCs for the new quest. The Monte Carlo simulator was great! It still has a few rough edges, but it already can summarize hours of test play in just a few minutes.
I fixed the scrolling to the hero after a quest victory and after entering a new board.
2023-09-15
The game finally landed on F-Droid. The build recipe is pretty straight forward: build Godot for Linux, then cross compile the export templates for arm. The tricky part is that Godot is missing a bunch of import metadata until you start the editor. Since I can't do that in the gitlab CI/CD, I export the game twice and that seems to solve all the problems.
I worked on placement constraints: for the next quest, I need to place two NPCs as far apart as possible to give the player a chance to prevent them from meeting. I got the region based constraints working (I can tell the monster deck generator that some monsters should always spawn on the North side of the map, for example). I should get the distance constraints working tonight.
I watched the preview event for Roguelike Celebration and the interview with the Diablo lead dev was so good!
2023-09-02
Revengate got it's first 5-stars review on Google Play!
Thank you, kind stranger.
It's now more obvious when you started a multi-turn travel. It's should also be more obvious how to cancel if a fat-finger-tap is what started the action. Maybe a bit of blur under the text would help with legibility.
Drag-to-pan is no longer super fast when zoomed-in.
Working on the new quest: Le Grand Salapou, the master mind clown spy is about to meet with an informant and you have to stop the encounter before it happens. Salapou is neutral, but easy to anger and his entourage tends to follow his lead.
A few new mechanics were introduced to make this quest possible. The global cross-faction sentiments are not variable. The Clockwork Automata have a guarding strategy: they follow Salapou around without getting in his way. They immediately retaliate if the person they are guarding is attacked. The informant can seek someone (Salapou in this case) and they will yield if you are convincing enough (that is, if you beat them up). JT wrote those last two.
I discovered ripgrep. Wow! It's awesome! It does a perfect job of only grepping my Godot files without looking at any of the auto-generated stuff. Makes me want to do a ton of refactoring now.
Next up: work on the dialogue and conversation triggers for the new quest.
NO BLOCKERS!
2023-08-25
The quick attack button shows how many weapons you have when you equipped from a stack. The button icon is updated whenever the active weapon changes.
This fixed several usability issues in one go. That was time really well spent!
I fixed the pan: it's no longer super fast when zoomed it. The board is pretty much sticky under your finger, which is what Material Design recommends. It's not quit there yet when doing a two fingers pan. I may or may not spend more time to get that to work as well.
I got the game to build on the F-Droid infra. It's in review now and it should get accepted. Fingers crossed!
The recipe is actually very straigthforward: build the Godot editor for X86, then the Android export templates for ARM (both the C++ and the Java part). If you've never launched Godot in interactive editor mode, it will complain that a bunch of files have never been imported and it will ignore them. Compiling the APK twice in a row fixes that.
There was a lot of fiddling because different parts of Godot want to learn about what you are trying to do from different sources (command line flags, env vars, config files). I will probably open issues with Godot to normalize that a little bit. At least the Godot configs are standard .ini files that the Python configparser
module knows how to deal with, for the most part.
There are just a few more minor usability issues to tackle this release, then I add the new monsters.
NO BLOCKERS!
2023-08-19
I submitted Revengate to F-Droid. F-Droid is a very interesting app-store for Android that only catalogues open-source packages. They are so serious about this mission that you can't send them your signed binaries like you do on Google Play β you have to provide build instructions so they can build it from source themselves, including all your dependencies. In the case of Revengate, that includes building Godot as well. Thankfully, their build environment is Ubuntu, which is also what I run on my dev machine. I'm running slightly different version of the OS, but it's close enough that I feel good about what I submitted even though I didn't run the build pipeline on their Docker images. Revengate also seem to be the first Godot 4 game on F-Droid.
UX is my least favourite part of game development, but seeing a few testers struggle with their first exposure to Revengate motivated me to spend the next two releases on smoothing things out a little bit.
I did my best to identify and quantify the current interface problems, then I scheduled the most promising solutions in my roadmap. Of course I can't only do UX β that would be a reliable way for me to burn out β so I also plan on adding a few new monsters.
I started working on the quick attack button with JT Wright.
It indeed streamlines combat! It's the same number of taps as tapping on the monster, but the new button is always right under your thumb and you don't need to aim.
NO BLOCKERS!
2023-08-12
The monsters Tracking strategy is now triggered by perception: a monster will not start tracking a foe until they can perceive them. They will keep tracking for a while after loosing perception, but they stop tracking if when it's been too long since they sensed their prey. If you have better perception then them, you could retreat ASAP when you sense them and thereby avoid them entirely.
Most monsters are roaming, so you still bump into then, but at a lower frequency. This feels really good and it pushes the player to balance the trade-offs of side exploration vs direct path to the next dungeon level.
I smoothed most of the rough edges of items grouping with JT Wright.
I published v0.9.3 on Google Play. It includes this, the ranged combat from last week, and the healing spell.
Next up? I think it's time to add a new quest.
NO BLOCKERS!
2023-08-04
Similar items in the inventory screen are now grouped together.
There are still a few edge cases to take care of, like activating an item that is part of a stack. Presumably, you don't want to light the fuse on all your dynamites, so we should probably make it obvious which one is now lit so you can drop only that one.
Wildfires got really bad in Spokane and I got evacuated to a level 3 alert (go now). The fire got to about 1km of my airbnb and I was mentally prepared to lose what I had left behind. I was able to come back and all my stuff is fine. I'm still under a level-1 evacuation watch (be ready), that is: go-bag by the door. That adventure messed with my focus, but it got me to think about what is important in life: mechanical keyboard or water filter?
NO BLOCKERS!
2023-07-29
I was out camping in Yellowstone this week, so not much progress, but I feel greatly energized!
I added tile highlight to show where there is a default action for something. The default action is activated by a single tap: attack an enemy or talk to a friendly NPC.
JT Wright implemented ranged combat.
JT was really eager to push the improvement, so he pressed Enter
as soon as he heard me say git reset --hard
, before I had a chance to mention the name of the file to reset. He lost the first implementation of ranged combat, but he didn't let that get his morale down and he re-implement the whole thing right away. What a trooper! We only support ranged weapons that are their own ammunition (ex.: daggers) right now. Next stop, ranged weapons with separate ammunition supplies (ex.: bows, guns).
I paired with JT to make a lightning spell. The spell stuns, which is scary effective! We still need a sound effect for that one and there is a magical algebraic spell that we need to unlock to stretch the zapping bolt from the caster to victim.
NO BLOCKERS!
2023-07-15
I added a healing spell.
The visual effect for this one is a Godot particle system, no custom shader code. The sound effect is me singing. I pitch shifted several copies of the clip to simulate multiple voices before adding some reverb. Audacity has a reverb preset for "cathedral", which seems like an appropriate venue for a choir of angels to be singing in.
2023-07-10
Adding the Monte Carlo simulator went really well. With great foresight, I had written the main loop without assuming that one of the actors would be controlled by the player. I was therefore able to call that loop unchanged in the simulator. Furthermore, all the monsters were really happy to beleive that they would be unseen if there was no hero at all in the game (rather than the hero being out of perception range), which means that they just didn't do their animations in simulation mode. Sounds and a few minor animations were missing that perception gating, but that was loudly obvious and easy to fix.
Then it was just a matter of making the main loop restartable and to add a few more combat signals to make the simulation stats more detailed.
The performance turned out to be better than I was expecting, between 1000 and 2000 combat turns per second for most monsters. I tried the Godot profiler for the very first time to see if there was something obvious that could be improved.
The Godot profiler is interesting. The overhead is pretty small, about 20%, but there is unfortunately no call graph. The stats are aggregated per rendering frame and you can go back in time. So if there is one frame out of 20 that is slow to draw, you can click on the graph where the drawing time spikes and bam! You see the per-function time break down from back when this frame was drawing. So cool!
For my combat code, there are plenty of small gains to be made, but they would all require some non-trivial changes so I'm resisting the great urge to fiddle and not optimizing anything right now. Tuning the balance is more urgent than speeding up combat calculations.
A simulation run looks like this:
Starting a batch of 100 sims for AverageJoe, Rat, Rat2, Rat3...
Ran 100 sims in 1.26 seconds (421.64 turn/s)
Median encouter lasted 5 turns
Victory: 99.00%
median gladiator health: 85.00%
Defeat: 0.00%
Draw: 1.00%
median gladiator health: 100.00%
median challengers health: 100.00%
AverageJoe's hit rate: 67.76%
Rat's hit rate: 76.07%
Rat2's hit rate: 72.85%
Rat3's hit rate: 78.28%
You can think of the "gladiator" as the hero, but it's controlled by the sim rather than by the player. It's a perfectly average human in this case and he's not great at hitting rats, but he kills them fast when that happens. That fits what I wanted to model: rats are weak, but small and hard to hit. (I don't know what happened during that sim that resulted in a draw.)
Then there is this one:
Starting a batch of 100 sims for AverageJoe, Ghost...
Ran 100 sims in 1.36 seconds (1221.98 turn/s)
Median encouter lasted 17 turns
Victory: 0.00%
Defeat: 100.00%
median challengers health: 81.08%
Draw: 0.00%
AverageJoe's hit rate: 11.74%
Ghost's hit rate: 86.67%
Ghosts are meant to be elusive and hard to hit, but this is obviously too hard. Thank you simulator, now I know where to do some tuning!
Next stop, auto-tuning...
NO BLOCKERS!
2023-07-07
Revengate v0.9.0 is on Google Play (for some countries). This build has the magic from last week, silver weapons, and a second quest!
Building the second quest was a lot of fun. I had to rewire a few things to distinguish between a chapter victory from a game ending victory. The new set of dialogues is starting to give some personality to the NPCs, and the quest reward feels great! There were a few UI changes and usually I find all sorts of excuses to procrastinate when it comes to UI, but for those the motivation of seeing the new quest was strong enough to get me to do UI first thing in the morning!
Tuning the balance has been very time consuming. I don't mind playing the game, but doing it non-stop just to move a few numbers is too inefficient. It's time to bring back the Monte Carlo Simulator!
The old Python version of the game had a perfect separation between calculations and presentation, so it was really easy to make a simulator for it. With Godot, presentation is coupled more tightly, but I think that the work I did on perception and visibility should be enough to run combats faster than game-speed (monsters that you can't perceive move without animations).
In any case, getting that working is my priority for next week.
NO BLOCKERS!
2023-06-30
Revengate has magic!
This is pushing what GIFs can do, here's a better encoding of the demo.
I kept pushing back on this one, partly because I was not sure about the magic system (scroll all the way down), which is a bit of a fusion between the systems in Wheel of Time and American Gods with a steampunk twist. At some point, I figured that it's probably better to have lots of an OK magic system than just a tiny bit of a great one.
OK, I went a bit wild with the summoning spell. If you squint, you can probably guess how it works. The key is picking good looking decals to start with, then it's just a bit of pixel shifting with some colorization.
About half of the magic rules are implemented (the phantruch are bound to a vital assemblage and there is a channeling device to decrease your mana cost). Only monsters can do magic for now β I need a few more spells before I can make up my mind on how to expose the whole thing to the player.
2023-06-24
You can open and close doors. There are new "vibe" nodes, things you feel while walking around the dungeon.
Those are not used by the level generator yet; they will probably come in a late pass and be tied to active pre-fabs and to whichever monster cards were drawn for the level.
Added end-of-game stats: easy stuff stuff like the number of turns and the list of victims.
Added silver weapons: better to-hit against ethereal monsters.
async vs sanity
My brain almost melted trying to figure out why items sometimes didn't reappear after being stepped over. This was a really rare thing, maybe 1% of the times. Playing with the duration of the actor move animation and the item fadeout animation allowed me to repro about 25% of the time. I was happy about that and I added a bunch of print statements to see what was going on. Just like that, the problem was gone! The time it took for Godot to print was slowing things down enough to perfectly resynchronize everything.
Change of strategy: sprinkle asserts without printing anything in hope of catching the problem and getting a stack trace when the problem happens. It turns out that there can be quite some time between then the first and when the last handler for a signal are called.
When an actor is still in running their animations, they can end their turn and let other actors play. This increases the game pace and is more fun to watch. It order to keep the game consistent, however, that actor can't start their new turn until they are done with their animations. So one signal handler on Tween.finished
decrements the active animations counter and then fires another signal to let the game loop know if the next turn can start for this actor. If the animation is a move, there is another handler on Tween.finished
to do more internal cleanup.
So once in a blue moon, when the game loop sees that the actor is good to go, the next turn starts for that actor, the actor begins moving, and about half way through their move, the original move cleanup handler for the previous turn fires. Damn! That messes the internal state of the active move and the item on the floor suddenly thinks that someone is still stepping on it.
So moral of the story is that you can keep adding stuff to Tween.finished
, but if you depend on a reliable timing, then all the important work has to be done in the first handler that you register.
2023-06-15
JT Wright made a really cool particle system for the dynamite.
You now perceive monsters only when you are close enough.
A direct line of sight allows you to perceive further, but it seems just fair that you'd be able to smell or hear monsters hiding just around a corner. There is a new potion of booze that will increase your regeneration for a little while but it mess with your perception (among other things). Items are always visible, this is a conscious design decision to torment the player with known potential rewards vs the unknown risk of getting beaten up.
All of this plus last week's progress seemed like a substantial enough increment so I pushed v0.8.0 to Google Play.
2023-06-10
Revengate has an overworld map.
While crossing a traboule, stairs and gates mostly move you towards the cardinal point where they are located, but the map is disabled underground to keep the experience slightly disorienting.
I'm now smarter with where stairs are placed by maximizing multiple Dijkstra minimums. Stairs used to be placed randomly and that makes a good level on average, but there were too many cases where up and down staircases would be right next to each others, allowing the player to completely bypass the exploration of that level.
Now after placing a cross-level connectors (stairs and gates), I measures all the distances from it using the Dijkstra algorithm. Each subsequent placement picks a cell where the minimums of previous Dijkstra metrics is maximal. This is probably easier to understand with a picture.
Here the gate on the right is placed first since it has a stronger constraint: it must lead to a location East of here. The Dijkstra metrics from it are in the first matrix. The stairs going down are easy to find: distance 41 at top left. We make another set of distance measurements starting from there.
Things start to be interesting when trying to place the upstairs. For each position on the board, we find the minimum from both Dijkstra matrices at that position. The optimal placement is where those minimums is the largest: dist=25. This is also in the top left, but quite the detour away from the downstairs. Ties are typically numerous, especially with large rectangular rooms. Those are resolved by picking a random maxi-min coordinate.
2023-06-03
I have pinch to zoom!
This was not as hard as I was expecting it to be and the improvement to the player experience is hard to overstate. My old zoom slider bar was either hard to hit or it took too much of the precious mobile screen real estate. It really paid of that I was scared by this change and that I sat down to flesh up the whole plan on paper before even looking at the code.
Godot does recognize some multi-touch gestures in the internal API that it uses to build the editor, but unfortunately none of that is exposed in GDScript. Godot exposes touch events with an index
attribute that is unique to each finger and you need to keep track of previous events to decide for yourself if you are inside a gesture. When you receive a tap (InputEventScreenTouch
) with index=2
(third finger on the screen), there is no information in GDScript to tell you if the other fingers are still on the screen nor where they were last time you saw them. Hopefully, you've been recording that back when those events fired. Another "interesting" quirk of the Godot API is that Buttons are blind to touch events. Buttons just don't see taps at all. You can enable mouse-emulation, but then you receive both a mouse event and a touch event every time a finger does something on the screen. The only way I found to take care of this was to detect the platform and ignore mouse events in the gesture recognizer on mobile. This is OK, but it means that a player who paired a bluetooth mouse with their tablet will be stuck to do all the gestures on the touch screen.
With something like Google Maps, zooming and panning is the same multi-touch gesture. If your fingers are moving but keep the same relative distance, then it's a pan, if their distance change but that the centroid of all fingers stays in the same spot, then it's a zoom, if both happen at the same time, no problem: zoom + pan. A multi-touch gesture is two or more fingers on the screen, and thinking too hard about how to track the motion of each fingers can easily melt your brain. I think that the key to stay sane is to only track the centroid of all the fingers and the average finger distance to the centroid.
So that's what I did and I almost shed a tear of joy when it just worked (ignoring the velocity tuning).
I added prefabs with a definition string telling me where and what. For example, adding a river (r
) on the East side of a new game board E
will be: Er
. That should make it easy to define the Lyon surface in terms of its major features in a 10x10 matrix of prefab strings. Prefabs all act on a rectangle and they return the sub-rect that was not fabbed. Multiple passes of procgen can therefore easily avoid stepping on each others toes.
PyPI, the Python Package Index, recently deprecated GPG. Reading the really well written annoucement and it's associated links sent me a bit down a rabbit hole. In the end I could only conclude that it was not just me who it not smart enough to remember how to manage my GPG keys, it's that the whole thing sucks. The usability problems will be obvious to anyone who tried to do anything with GPG, but the crypto happens to be a problem as well. It has not been broken, but the security guarantees provided by GPG is incredibly weak compared to something more modern like Signal that will have one key pair per person you communicate with and that will change it every single message. A compromised GPG key would be the failure of the whole system while a compromised Signal key is no more than one compromised message.
Of course, Signal is not a solution for code signing, but I was really happy to discover that OpenSSH can do that part and that it's been integrated with git. It's a little weird that they call the ssh signing gpg.format
, but the UX is perfectly transparent otherwise and both GitHub and GitLab will show those ssh-signed commits as "verified" like they do for gpg-signed commits.
Things get really cool when you add age to the mix. With age
, you can take the public ssh key that GitHub and GitLab displays for my commits and encrypt a message for me with it. You have a really good sense that the key belongs to me since I signed all this code with it, so if you can't meet me in person for a key exchange, you still have a solid option for an initial contact.
My GPG key is expiring in 11 months and I don't think I will bother to extend it or create a new one after that.
My laptop battery died: it's all poofed up like a bag of chips. Thankfully I use a System 76 computer and all the service manuals are online. It was really easy to take it out while I'm waiting for the replacement and doing it myself does not void the warranty. I'm stuck staying plugged in the mean time.
2023-05-27
Revengate v0.7.0 is on Google Play. It features a new quest that is starting to show some steampunk vibes.
Levels are populated with monsters and items using card decks. Last week focused on teaching the deck generator about world locations so that the quest item and boss would be generated when you end up in the right part of town.
Now that the above is out of the way, I fell ready to tackle my nemesis: multi-touch gestures. Let's see if I can get pinch-to-zoom to work better that the magnification slider-bar.
2023-05-20
I moved to Lyon, France and I'll be here for another seven weeks.
Revengate is set in Lyon and I was at the point where I was designing the surface levels when it dawned on me that Google Maps was not the right way to immerse myself in the vibe of the city. So in what could be the greatest yak shaving of the decade, I packed my bags and came to see the traboules in person.
Unsurprisingly to many of you, this exploration probably won't have much influence on the level design: the low graphics style is meant to let your imagination run wild rather than depicting things in great details. The influence on the quests and stories, however, will be profound.
I had greatly underestimated how much mystery and intrigue was hiding around every corner of those narrow twisty streets. Secret societies, black magic, knights templar: they all had a well documented representation in the city for centuries. More on that in future updates once I start integrating those ideas in the storyline. For now, the real Jacquard loom completely blew my mind. This programmable machine will receive a steampunk twist earlier in story line with punch cards being commonly used to represent hidden messages in addition to weaving patterns.
I played with the tile atlas to depict a cleaner surface world and darker and older subterranean world. Most of my work during the past few weeks went into eating cheese, err, I mean allowing the procgen to know what level design to use depending on where you are in the world. I want to have parametric prefabs to decorate the obvious features like the two major rivers, but for now the transition between the two vibes is working well. A traboule dungeon correctly links two parts of the surface city.
2023-03-25
I added a maze generator.
It's reasonably tunable with various biases: should corridor be long and straight or short and twisty, how often do you see dead ends, is there more than one path between any two points? It also supports masking, which is another way to say that I can tell it to ignore arbitrary areas of the board.
I had a tunable generator in Python based on the Recursive Backtracker algorithm. For this one, I went with the Growing Tree algorithm. Growing Tree is a only slightly more complicated, but it enables the bias for more dead ends, which was impossible to control with the Recursive Backtracker. Since one is recursive and the other is iterative, I didn't review my Python code and just started typing in Godot. I was pleasantly surprised that it worked on the first try.
Even though I didn't look back at the Python code, writing it really helped me get a good intuition on which biases gave a good return on investment. It's weird, but there is a part of my mind now that thinks like a maze generator. When I look at a street map, I see how it would be like with different bias parameters.
Both algorithms and many more are covered in Jamis Buck's excellent book.
I finished reading Shadow of Self. What an ending!
2023-03-17
I added a dynamite item.
This was a big refactor since it's my first item with an expiration time. I standardized how things age and now I expose the same API for Strategies, Items, and Conditions. That allows me to age everything without knowing their type. I also disabled animations for things that are not on the active board, so before returning to a previously visited level, I can quickly refresh all the conditions and blow up any lit dynamite you left behind before showing you the level. Disabling animations is going to come very handy when I resurrect the Monte Carlo simulator.
I couldn't find a good sound effect for the explosion, so I ended up voice acting it.
Turns out Godot did not enable Android gestures in 4.0. What got enabled is a limited Kotlin API that is use by the Godot editor on Android. Long story short, I will have to implement gestures in GDScript. I'm saving pinching for later, but I was able to do the long tap in just a few lines and a Timer. I used it to implement a context menu. Feel free to force yourself to attack the quest NPC. He's got good loot.
2023-03-11
I started playing with shaders and I'm really happy with how the water surface and shockwave VFX came out.
Godot shaders are 95% GLSL. There is a tiny bit of syntax extension to help you tell Godot what kind of data your shader is expecting, which in turn lets the editor give you a fancy point-and-click UI to supply the shader data. Also, rather than having main()
be the entry point for both the vertex and the fragment shader, they start with vertex()
and fragment()
respectively, which means that you can put them both in the same file. Nice! The editor does an excellent job of reloading the shader preview as you type β you don't even need to save β which makes experimentation very dynamic.
I finished implementing the parametric card deck rules and I can now compose them as draggable Godot nodes to specify what goes inside a dungeon. It works for both monsters and items. It does not have cross-cards dependencies yet, which is something I would like to add at some point: it just seems fair that you would not encounter ghosts until at least one silver weapon has been generated.
Multi-turn commands can now be cancelled.
2023-03-03
Revengate 0.5.0 is available on Google Play and for manual Android install. This version speeds up the game play by having more animation overlap. Rats are more interesting: they mostly follow walls and will generally leave you alone, but if you bother them, they can either attack back or run away.
I paired a bunch with JT Wright on the rats behavior. We started working on more interesting deck based generation for monsters and items to support more rules than the current "spawn cost", like floor limit and uniqueness. Most of it is still too rough for this release, but it should make it into 0.6.0.
Minor UI changes include messages flashing briefly over the game screen and an hour glass overlay letting you know when it's not your turn yet. There are now in-dialogue actions, so be sure to have a chat with the barber β he might have something for you.
This felt like a good streak since the last release. The only time I really started procrastinating was when I was trying to style narrations. I was able to recognize what was happening early so I dropped those from this release and went back to working on bot strategies, which is always a great source of joy. Having blockers for news sites really helped me to see that I was procrastinating more than usual. Blockers are easy to disable, but disabling them is just hard enough to bring a sense of realization.
Godot 4.0 came out. There's nothing in this release that I was not already using from the latest Release Candidate, but it's really good to see that we finally hit that milestone. I'm already extensively leveraging the new TileMap editor and I'm looking forward to start playing with the new 2D lights in 4.0.
I finished reading Mortal Engines. The air battles were quite good! I started reading Shadow of Self, the second book in the steampunk cycle of Sanderson's Mistborn series. It's really good! There's a deeper detective twist to the story and the two sidekicks get their own perspective with a better character development. The whole thing is satisfyingly wrapped in metal-base magic, with a broader showcase of abilities. Nice!
2023-02-24
Monsters are now generated using a "probabilistic deck". Rather than storing multiple copies of the monster cards, we only store the number of copies, which can be a float (you need >=1 for a draw). This is faster and more compact. It could be tuned a tiny bit to keep track of cumulative weights, then a draw can be done in O(log n).
I managed to wrap my head around how the Dialogue Manager plugin tracks the conversation flow and got conversation options to work. Actors also remember where you left off and will potentially start right there if you talk to them again. Conversation actions, like giving someone a potion of regeneration work!
Rats now have a strong bias towards traveling by hugging the walls. This was consuming me all week: how to do it cheaply without completely blowing up my A* implementation. I played with different metrics on paper. I found one that seemed promising, implemented it, then it just worked! It's a slight bias against diagonals and a bigger penalty for going somewhere with fewer walls, only counting 4 of the 8 adjacent cells. It turns out that Manhattan distance is a really good way to estimate the diagonals bias, so I can use that as my heuristic in A* and keep most of the A* bad paths pruning properties.
I worked with someone to add a FlightOrFight strategy on rats. It's pretty good already, but it needs a bit of tuning before players can start exploiting rats as decoys.
I revisited my nemesis: animations overlap. I stopped waiting for animations to be done before moving the turn counter. This really speeds up the game play! There are still a few cases that explicitly wait for anims, like an actor has to finish moving before they can take their next turn action. Strategy.act()
returns a bool rather than an animation; the few places that need to sync on the anims have to poke the Actor state machine. There is room for more tuning, but it feels really good right now and I don't plan on playing more with it for a while.
Godot released 4.0 RC4 (then RC5 two days later). This is a big deal for Revengate because many of the Android gestures are now recognized by the engine. I should be able to replace the stupid zoom slider bar with pinching soon.
I played a few games of One Deck Dungeon, a table top roguelike. It's hard! The solo mode is pretty fun. It's also probably longer that the advertised 30 mins for most players. I don't think that there are any mechanics that would port to Revengate, unfortunately. I like the anti-grinding design: any action costs time, which brings you closed to a deeper dungeon floor with more challenging encounters. This is a lot more fun than hunger.
2023-02-17
Revengate 0.4.0 went out to Google Play (also available for manual side-loading). This release includes the monsters stat page, monsters who remember what happened to them, non-aggressive rats, dialogues, and a skills system that is not exposed to the players yet, but which already reduces the number of misses during the typical encounter.
The Godot Dialogue Manager plug-in is excellent! It makes it really fun to write the dialogues and it has all sorts of plug points to let the conversations change the global game state. Integrating it is some work, however, and I have yet to take advantage of all the features.
Actors remember stuff that happened to them. This allows more interesting monster strategies, like self-defense (done) or fight-or-flight (todo).
The 3-stage skill system can be an excellent basis for character progression and it would map well into a skill tree (ex.: get Advanced with Blades before you can unlock Rapiers OR Sabers). For now it only decouples agility and to-hit, which results in fewer misses during the typical encounters. I much prefer those shorter and more eventful encounters.
I could not find spooky enough sound effects for the ghosts, so I started recording my own. I'm quite happy with the results! I used Audacity with very minimal manipulation of the voice acted samples. With a dozen takes in a row, one is bound to be good.
I worked with a contributor to revamp the behavior of rats. Mostly, they don't care about you unless you bug them. There is a lot more to come, like the difference between them spotting you from a distance vs being startled when you surprise them.
I upgraded to Godot 4.0-RC2. It's mostly about stability and polish now. The engine is more enjoyable every week.
Since the release, I also added multi-attacks. The result is brutally effective. Some tuning will be needed with this one.
I finished reading Allow of Law: so good! The book is completely action packed with a very interesting and very steampunk magic system (the Wild West flavor of Steampunk).
I started reading Mortal Engines. The steampunk concept is pushed much deeper in Mortal Engines. It's yet another steampunk story that is happening in London, but this is a much much different take on London. It's generally enjoyable, but nowhere close to be as good at Allow of Law. I don't think I will go through the whole series.
2023-02-03
We have a bestiary!
I paired with a new contributor to implement monster spawning based on a per-level budget and spawning decks. This keeps the per-level difficulty equal whether there are lots of easy monsters or just a handful of really hard ones. That is of course assuming that we can get the difficulty metric right, which is probably not true right now, but I'm confident that we can get there with the Monte Carlo simulator.
We currently have spawning decks with duplicate "cards" of easy monsters. I want to reimplement those with single instances and the probability value instead. Those will come in handy for many other aspects of procgen.
2023-01-26
Lots of minor loose ends to tie this week for the v0.3.0 release. This release features an actual quest that felt surprisingly well balanced despite most of the key numbers coming from wild guesses. It's pretty hard the first few times, then you figure which items are worth picking and it becomes probably winnable 2/3 of the times.
The highlight of the week was implementing healing, both from innate regeneration and from items. Regular items won't heal you more than your full health level, but magical one can go above your max (ahem quest hint ahem). The items design fully embraces the Godot node composition paradigm and quest designers can drag and drop a bunch of effects on their new fancy items.
Next week, the work on the real fun stuff starts: bestiary and/or dialogues.
2023-01-21
This was another good week. The highlight is definitely that after installing Godot 4-beta13, I discovered that deploying to Android automatically starts the remote debugger, which makes it ridiculously easy to inspect and manipulate the scene tree of the app running on the phone.
I don't know for sure if it was already there, but I know I will take advantage of it a whole lot more from now on.
I added effects and conditions for damage over time, like poison and being on fire.
I added cheat codes, like teleporting and printing the internal data for a given location, but with the newly discovered power of the Godot remote inspector, those won't be as useful as I originally thought they would be.
I added victory conditions, so you can finally win the game rather than infinitely bashing monsters. I have a few things to tidy up, then I will probably push a v0.3 build to Google Play next week.
I started reading The Alloy of Law by Brian Sanderson. That's is a really interesting take on steampunk magic. As expected from Sanderson, the magic system is very elaborate with obvious limitations. Without spoiling it, some people are born with the ability to influence metals. In medieval time, that was probably cool but not that useful, in the middle of the industrial revolution, that's obviously game changer.
2023-01-13
Items: items fade away when stepped over. They can be picked up and dropped. I'm still working on the inventory dialogue. I have something working, but it's kind of clumsy.
Godot supports buttons in tree-view records and it does a good job at dispatching input events to them, but it only allows sprite buttons, so that's a whole new asset class to maintain. I suspect that it's possible to extract the textures from regular buttons to make the whole thing follow the lovingly crafted UI theme, but I have yet to unlock that spell.
Items don't do anything yet, but I have a fairly good idea on how to do that part. It will be very similar to what I had in the Python implementation of Revengate.
Next week: victory conditions; maybe: items for progressions and damage over time.
2023-01-05
Revengate 0.2.0 is ready for play testing on Android. That's my first public build of the Godot reimplementation! It doesn't do much, but you can beat up monsters and go infinitely deep in the dungeon. Google Play will probably take a few more days to show the latest version, but the manual APK install is ready for action.
What level did you reach?
As I was making zooming and panning more mobile friendly, I got really confused by how events were processed. It just didn't seem to match the way it was described in the doc, so I set up a few experiments with lots of logging to really understand how Godot handle user input. Here's what I found.
All input events propagate through 3 main phases:
Node_input()
in reverse traversal order for all nodes in the scene;Control._gui_input()
for the control (widget) where the click was or for the control with focus in the case of keyboard events;Node._unhandled_input()
in reverse traversal order for all nodes in the scene.
Here, "reverse traversal order" means that we start at the bottom of the screen tree as rendered in the editor and go up one line at a time.
Disabled controls still get _gui_input()
and _unhandled_input()
. Invisible controls pass _gui_input()
to their parent, they still receive _unhandled_input()
. The SubViewportContainer receives the _gui_input()
for the Viewport.
Calling either Viewport.set_input_as_handled()
or Control.accept_event()
has the same effect: it immediately stops the propagation of an input event. They can be called from any of the _*input()
methods and they do not need a reference to the event. Accepting in the _gui_input()
of a control will block the built-in behavior of that control. For example, calling accept_event()
in MyButton._gui_input()
will prevent the pressed
signal from being emitted.
The z_index
property of a node can make it to show on top of other graphical elements, but it has no effect on which control will receive the _gui_input
. Only the order in the scene tree can change that.
I also ran into an interesting problem: SubViewports do no remap user-defined InputEvents. That forced me to understand Transform2D, a 2x3 matrix that is a property of all Godot Node2D objects. A position vector can be multiplied with it to translate, scale, and rotate it, but since it's the transform to render a Node2D, inverting the matrix is often required depending on which coordinate system you want to remap to.
While pulling my hair (figuratively) to figure in what order I had to apply the above transform, I adopted following convention in all my logging messages:
- pixel coordinates are noted "(x, y)"
- tile coordinates are noted "[x:y]"
This really helped!
I got rid of spacial audio for now, but I like the concept. I will definitely revisit it later.
I started reading Leviathan Scott Westerfeld and Keith Thompson. Now that's one damn good steampunk novel! The main premise of steampunk is that some kind of technological advance came in early during the Victorian era, but everything else about social dynamics is pretty much the same. In Leviathan, you are greeted by bioengineering and Gundam-style mechas during the 19th century. Wow!
2022-12-30
I implemented zooming and panning on desktop.
This is a done by combining a viewport with a camera node. The viewport does a good job of remapping the mouse and touch events to its children nodes, but it's not easy to move it around. The camera is easy to move around and it has a zoom property, but it does not offer any support for remapping events, so I only use it to change the center of the screen. The HUD (health bar and other status info) is outside of the dungeon area viewport and is therefore not affected by zooming and panning. This was a beautiful example of where the Godot node tree metaphor works really well.
I though that it would be a trivial matter to have those on Android given the refactor that Godot just merged in for gestures, but for some reason I don't get any of those gesture events with my Android builds. It not too hard to hard to make a gesture detector in GDScript, but that would also involves fine tuning what counts as one gesture rather than another. I'm not going down that rabbit hole right now.
I merged the Godot implementation back into the main git repo. There are still a few design docs that should be updated to clarify if they refer to the Python or to the Godot code.
I added the art nouveau steampunk boot splash. Unfortunately, it does not show on Android, which is probably caused by this issue. The game starts really fast on Android anyway so I probably don't need a boot splash. I will find another way to showcase the artwork without making the players wait for purely aesthetic considerations.
I got a fnatic ministreak keyboard with Kailh silver switches. I took the time to lubricate and install o-rings on all the main keys. I knew this would not be main keyboard since I grew heavily reliant on a trackpoint, but that seemed like a cheaper way to see if I like the silver switches.
In short, I don't like silvers. I like the short travel and the ease of actuating them. For prose, that's actually really awesome. For code, I end up making a lot of typos as my fingers feel the board while searching for non-letter keys like the arrows. That means I end up looking at the board more, which defeats the purpose for me.
Specific to the ministreak, the keyboard does not support multiple layout layers and is not reprogrammable on Linux. That's a show stopper for me. The build is solid otherwise, but spacebar is also too wide for my liking, which forces an exaggerated wrist twist to get the control and alt keys.
2022-12-24
This was a really good week. I finished connecting the dungeon levels via stairs. The bugs came from non-active levels still having active physics layers. It turns out that in Godot, to make something disappear, you have to make it invisible and deactivate all of its physics layers. Using the full Godot collision system for Revengate is obviously overkill since everything in the game is a trivial square, but the API is really nice to use, so I'm keeping it for now.
I added all the pre-game screens. The Godot GUI editor is really good! The styling is particularly well done. It's a bit of a click fest, but there is a preview screen and you can click on a control than in will instantly populate all the stylable properties for that control on a new editor pane. This is so much better than editing .kv
files for Kivy UIs.
The Godot layouts play fairly well with multiple resolutions. While Kivy forces you to nest everything into dynamically resizable containers, Godot has a simpler but equally effective approach. Since most controls in a game are around the perimeter of the screen, Godot offers 8 anchor points around the edge and then let's you offset in pixels from there. As it turns out, this resizes beautifully with multiple resolutions and in Revengate's case, it keeps the controls where they should be on a tiny mobile screen. Controls can still be nested into resizable containers, but you don't have to most of the time.
I made the first Android build. It's nice that Godot puts that configuration outside of the main project file. That way, the APK signing key does not end up in git. On the other hand, there are some configs for the Android build that would be nice to have under source control. I haven't figured out yet how to decouple them from the secrets.
After a bit of configuration and 2.5 GBs of dependencies downloaded, I have a one-click option to deploy to any Android phone with USB debugging enabled. Nice!
On this special beautiful cold winter day, may the RNGs bring you all lots of shiny loot.
2022-12-17
I worked on connecting the multiple dungeon levels together via stairs. Turns out that Godot 4 has a really good API for attaching data to tiles, but nothing to attach data to cells. It Godot terminology, that means that I can have new arbitrary fields on all the stair cases, but there is nothing to help me record the destination of a specific set of stairs. So back to storing all of that in an internal dictionary like in Godot 3.
I got it working, but alls monsters are behaving weird on freshly created levels. I think they might be colliding with the ghosts of monsters on the upper levels. I need to dig into that this weekend.
I played some Marvel Snap. It's a deck builder by one of the designers of Blizzard Hearthstone. It's pretty good, very fast paced, with a good mobile UI. There are way too many internal resources and currencies, typical of modern free-to-play games, but the deck building part is excellent. That got me thinking: what could deck building games bring to a roguelike (in the dungeon exploration sense, not in the Slay the Spire sense)?
I feel like one key part of what makes deck building fun is that you need to sacrifice. All your cards have big pros and some minor cons, but you can only take 12 of them with you. You need to make an agonizing choice. Having a limited number of slots for armors and rings encapsulates some of that, but most of the time, it's really obvious which helm is the best, so the choice is not agonizing at all. In many games, there is even a green arrow to help you identify which of your helms are better than the one you are currently wearing.
In a true steampunk fashion, I started thinking of a utility belt where you can store tools and crafted clockwork gadgets. The utility belt has limited slots and all the gadgets can go in any of the slots. You can have as many gadgets as you want in your inventory, but only a limited number of active ones on the belt. Gadgets would have a random chance to trigger during combat, then they have a cool down (rewinding?) period. Besides selecting the gadgets, you don't have any control on how they activate, that makes the pre-selection important. Gadgets abilities can be mild (+20% regen for 10 turns) or strong (disable a random foe's electrical abilities for 100 turns). Quick like that, that sounds really fun. It would not be too hard to implement, but the UI component would be critical if you want people to know that their utility belt did the work. On a tiny mobile screen, that could actually be fairly hard. I'm putting this design concept on the back burner for now and I commit to deleting Marvel Snap before the end of next weeks.
2022-12-09
I added taps as controls to move and attack. I also implemented the Travel To command.
Like in the Python implementation of Revengate, Travel To creates a Strategy on the Hero and passed the control to it. As long the strategy is_valid()
the game loop calls it rather than querying for input. I really banged my head on my desk to get this to work properly with Kivy and even then, the build I have on Google Play still has the nasty bug that some monster attacks are not detected, causing the hero to keep trying to push his way against a steadfast foe who will invariably beat him to a pulp.
Implementing it with Godot was a breeze. I was careful to have all my signals and state diagrams in front of me and I had the basic case working in less than an hour. I took another day to add the rerouting and cancellation upon being attacked. I cleaned up the main loop a bit in the process and almost completely decoupled the animation synchronization from the turn logic, which should really help when time comes to implement the Monte Carlo simulator.
The new anim synchronization probably does not have enough overlap between actor move animations, which gives a fairly slow feel to the game play. This will need tuning.
I should move the state diagrams to Graphviz. Paper is nice, but it's not very git-friendly.
I saw the new Tex Shura keyboard up for pre-order. I find this one very tempting and I spent some time de-mapping some of the keys on my bigger Shinobi to see what it would feel like to type with a minimal board. It's really not bad, having Home and End right on the home row actually feels quite liberating, even if as a two-key combo.
2022-12-03
I added a death animation and integrated the attacks in the monster strategies. I'm playing with some level of overlap between the animations when possible. Before attacking an enemy, I completely wait for them to stop moving otherwise the lunge animation looks off, but for moving around and attacking a stationary enemy, I think it speeds up the game play and it looks really good to have many actors moving all at once.
The tricky part is finding the right level of overlap and Godot Tween animation API is not completely helpful here. I can await
for the finished
signal on an animation and I can query the animation to know how long it's been running, but I can't know much longer it's supposed to take before it's finished. That make's it hard to do something like "overlap 20% of the animations", so I will probably settle for something like "overlap 100ms all the time".
On the plus side, it's awesome to be able to await
anywhere, which passes the control back to the engine for drawing pixels. This is way less convoluted than using callbacks, which is what I had to do most of the time with Kivy.
I typed the story of the encounter with a ghost and a cherub (scroll to "What Lurks in the Crypt", Gitlab is not very good with anchors). This will probably be used as a "cut scene" at the start of a campaign. I tried to hint at silver being a prime choice for fighting ethereal enemies without spelling it out too much.
2022-11-26
I figured how to take advantage of inheritable scenes in Godot. They follow a different inheritance tree than classes, which is a bit confusing, but it does make sense after you do a complete mental switch to the Node composition architecture.
I implemented the two-roll combat with most of the animations. I still need a death animation and a way to turn animations off when running the Monte Carlo simulator.
Godot 4.0-beta5 fixed the move animation flickering. Beta6 just came out, I plan on trying it later today.
I wrote a few stories of encounters with ghosts with a bit of a steampunk twist. I have not typed them yet and I still don't know if they will be featured in the game. I feel like coding when sitting in front of a keyboard, so I'm surfing the motivation while it lasts rather than typing handwritten notes.
2022-10-15
I can now procedurally generate levels with Godot.
The excellently revamped TileMap editor in Godot4 has a cool API to paint a whole area with a given terrain and it picks tiles of that terrain according to the probabilities that you configured in your TileSet
. It supports transition tiles to connect different terrains, but I have not started playing with that yet.
Next up: stairs and keeping track of previous boards, then randomly placing monsters on a new board.
I feel really good about that Godot rewrite.
2022-10-08
Moved to Godot 4. The latest GDScript has a per-element array comparison like Python tuples, which is exactly what I needed to implement multi-metric path finding. The 3-to-4 project importer does not do a very good job, but I had very little code to port so that was not a big deal. I re-implemented Dijkstra and A* in Godot and added path finding to monsters.
Godot has an A implementation, but since everything in my game is grid based, it seemed more natural to have distance discovery inside the algo than having to register all position with Godot's built-in before launching a path finding run, especially since the goal of A is to avoid having to process all the board positions.
There is a bit of animation flickering that started appearing on Godot 4. I'm assuming that this is a remaining bug that will be fixed before 4.0 gets out of beta and I'm ignoring it for now.
I really like the tile map editor in Godot 4. It makes is really easy to have multiple floor and wall tiles and to paint with the tile type, letting the editor randomize the tiles with configurable probabilities. It also makes it easier to attach data to tile types.
I'm ready to start working on procedural level generation next week.
2022-09-17
I'm still working on a Godot proof of concept. This was a good week! Turn logic is working. I played with the TileMap editor and got collisions to work, but monsters are still ignoring them.
I still have to get path finding to work properly. Godot as an A* implementation, but its API is a very verbose and you have to register all the positions with distance metrics with it. I think I will re-implement it to take advantage of the simplicity of square tiles. That will also allow me to keep the same API for the Dijkstra level metrics.
Godot has released the last alpha of 4.0. It looks like the final 4.0 could be out before the end of the month. There are two things that I'm really excited about 4.0.
- The new tile map editor.
- Better support for multi-touch gestures on Android.
Looks like pinch-to-zoom is going to be almost free after all!
Next week: progen the game boards.
2022-09-09
Spent the week exploring Godot again. Got some simple grid-based movement working. I reimplemented some functions from the Python random module in GDScript. The biased choice (like random.choices()
) was surprisingly easy. Having a built-in binary search that tells you the index where the search failed really helped!
The latest Kivy build is still on Google play. You might have to be part of the testing group to see it.
I'm now fairly confident that the above the last Kivy release. I'm going to push hard for something playable with Godot soon, even if it does not quite get to feature parity with the Kivy implementation.
2022-09-03
The build from last week is still on Google play. At least one tester confirmed that the Game Over Screen crash is no longer happening. You might have to be part of the testing group to see the test builds since I didn't release the public yet.
I really don't like pushing to Google Play since the builds don't show up until someone from Google goes through the game in depth, which takes about 4 or 5 days. They really play the hell out of this. The last report they game me contained almost 500 screenshots with annotations on which messages were not accessible with a screen reader.
I've been playing with Godot this week and I really like it! I didn't really want to learn a new language and possibly have to rewrite everything, but GDScript is insanely close to Python. It's basically Python without a mark-and-sweep GC and with fixed size numerics to make it easier to pass data to shaders. It's a little more complicated than that, but still very expressive and it feels just right if you happen to like Python.
I got a tiny game going in a little over 2h by following a video tutorial and to my delight, it was a breeze to make it run on Android. It was just a matter to telling Godot where to find the Android libs and what key to use to sign packages. The tiny game takes only 8s to build and deploy on my phone. Nice!
So now I'm putting the Kivy implementation on hold while I actually do a legit rogue-like proof of concept on Godot. If it goes well, I will probably do a rewrite right away rather than pushing for a complete game with Kivy.
2022-08-27
Wrote the second long narration: the initiation to steampunk magic. It's a bit too long to be an on-screen narration, so I'm not sure how to use it yet, but I really like the vibes that it sets.
Fixed the game over crash. This one was a segfault, so very little useful info to help me nail it down. It turns out that Kivy does not support changing screens while animations are in progress. There is no easy way to synchronize async animations so the only thing I could come up with is sleeping for a little bit.
I started learning about Godot. It's a bit of a click fest, but the amount of eye candy you can get for free is really appealing. I'm fairly convince that I will want to reimplement the game on top of it, but I want to make a few proof of concepts first.
Pushed a new less buggy tech demo to Google Play. Should be available to here or here, but you might have to be part of the testing group.
2022-08-19
The Google Play page is finally live! You might have to join the testers group to see it since the app in still in pre-lauch mode. The build that made its way through the approval process is fairly buggy. I will upload something better in the next day or two.
If anyone tries it despite my above warning, there is a feedback survey.
What should be the icon for loot/pick-up? Ideally it would be a Material Design Icon.
Wrote the intro narration on how the hero gets recruited by Lux Co. It's missing a few paragraphs, but the key elements are there.
Besides learning how Google Play actually works, I fixed a few crashed and I worked on items as progression. There are no XPs in Revengate and I really like it that way. My goal is to provide plenty of items that will boost your stats in various ways, with quest items being more significant than regular loot. This should discourage grinding while still keeping a sense of accomplishment.
The game over screen bug keeps eluding me. I am pretty sure I will have to move away from Kivy sooner or later. Current contenders for the replacement include Godot and Qt, but I want to have at least v0.4 (two full campaigns) up before I start a tech stack change.
2022-08-13
I spent most of the week chasing a weird bug where the game-over popup sometimes does not disappear. I think that this is a Kivy bug, because I tried pretty much everything and it still happens about 25% of the time. I'm officially giving up: Game Over will be a separate screen for now.
Because I spend too much time on the Game Over bug, I didn't get to clean things up to do an actual play test, but I pushed a tech demo build to Google Play.
Let me know if you try it out!
2022-08-06
I got a new laptop and I actively procrastinated by setting it up and fine tuning everything.
I did a few minor changes to the Revengate mobile experience, like adding a scroll bar for the monster descriptions. I will be uploading the first play test to Google play as soon as I find out why the Game Over popup does not disappear after you click OK, sometime later today or tomorrow. I will update this post with the link once this is done.
Plan for next week
- scroll bars for monster description
- bug: selection sometimes triggers the travel-to command
- upload a play test on Google Play
2022-07-30
I got caught in a wave of low motivation when I started re-aligning stuff on Android. I can repro some of those by shrinking my window on desktop, but for the most part I need to push a new build to my phone, which takes about 30 seconds, just enough for me to zone out. There was also an ugly bug where one move animation from the previous turn would get mixed up with the attack animation in the current turn and the retreat from the attack would place the attacker in-between tiles. I already knew that about myself, but it was made very obvious in the past two weeks: I really enjoy working on combat mechanics, fine tuning pixel placements, not so much.
I the end I overcame the slump by booking a pair coding session to work on the healing animations with my friend Angel Hudgins. That fuel me with enough energy to work on some the pixels stuff in the second half of this week.
The game is still fairly rough, but I'm happy uploading for a broader play test sometime next week. I will probably fix a few easy bugs before that, but I really want to have something out before the end of the week.
Last two week's summary
- wrote an online survey for play testers
- re-aligned labels on mobile
- healing animation: absorbing HP numbers rather than having them fly off
- bug fix: animating anything related to an already dead actor would crash the game
- better control of the overlap between movement animations of NPCs through explicit synchronization. Currently set to 70% overlap.
- resolved the traffic jam problem using Dijkstra walkability metrics
2022-07-06
First time posting here. For the past several months, I've been working on Revengate, a rogue-like for Android written in Python. The game is set in a steampunk version of mid-19 century France with deep conflicts between magic and technology.
Game overview
There is a fairly complex backstory and a few campaign outlines, very little of which is actually implemented.
There are many unique monsters that are going to be featured with low resolution images or ascii characters during the normal game play. To increase the immersion without significantly increasing the development complexity, there is also a comprehensive bestiary with more detailed depictions of the monsters, something along the lines of the D&D Monster Manual or the Pathfinder Bestiary.
The game code is free and open source software under the GNU GPL v.3. The artwork uses a variety of GPL-compatible licences, mostly Creative Commons.
There are still many rough edges, but the game should be playable for a single dungeon in a week or two. Actual campaigns will come later.
2022-07-11 β 2022-07-15
-[x] travel-to command
-[x] Dijkstra metrics
Travel-to is implemented using the same strategies that already control NPCs. When starting a multi-turn action like travel-to, I give a strategy to the player character. The engine is perfectly happy to control any actors who are equipped with valid strategies. It's like magic! But I probably need to add a pauses because things happen a bit too fast when I let the engine take care of everything. Path finding is done with A*.
I had a problem with wandering NPCs causing permanent traffic jams if two of them were trying to reach their way points in opposite directions in a narrow corridor. I figured that the best way to solve that would be to discover a new waypoint that is guaranteed to be reachable. I'm doing that with the Dijkstra algorithm, which was fun to implement and will come handy for a for other things.
Extras
Here are few highly connected mazes followed by their Dijkstra metrics. The number is the least significant digit of the distance to the origin (bottom left). Note how diagonals correctly cost only one. The Revengate maze generator can be biased for more or less twistiness as well as for reconnecting corridors to decrease the maze difficulty.
.....................................β...............................β...................................β.............β
.βββββββββββββββββββ.βββββββββββββββ.β.βββββββββββββββ.β.β.β.βββββββββ.βββ.βββββββββββββββ.βββββββββ.β.β.β.βββββββββββ.β
.................β...β...........β.β.β.β...............β.β.β.........β...β.β.....β.............β...β.β.β.β...........β.β
.βββββββ.βββββββ.β.βββ.βββββββββ.β.β.βββ.βββββββββββ.β.β.β.βββββββββ.βββ.β.β.βββ.β.β.βββββ.βββ.β.β.β.β.β.β.β.βββββββββ.β
.........β.......β.β.β.β.....β.β.β.β.β.............β.β.β.β.β.......β.....β.β.β.....β.β...β.β...β.β.β...β.β.β...........β
.βββββββββββββββββ.β.β.β.β.β.β.β.β.β.β.βββββββββββ.β.β.βββ.β.βββββ.βββββββ.β.βββββββ.β.βββ.β.βββ.β.βββββ.β.β.βββββββββ.β
.β.................β...β.β.β.β...................β.β.β.......β...β.β.....β.β.β.......β...β.β.β...β.....β.β.β.β.......β.β
.β.βββββββββββββββββ.βββ.β.β.β.β.β.β.βββββββββββ.β.β.βββββββββ.βββ.βββββ.β.β.β.βββββββββ.β.β.βββ.β.βββββ.β.β.β.βββββ.β.β
.β.β.....................β.β.β.β.β.β...........β.β.β.........β.........β.β.β.β.β.........β.β.......β...β...........β.β.β
.β.β.βββββββββββββββββββββ.β.β.β.βββββββββββββ.β.β.βββββββ.β.βββββββββ.β.β.β.β.β.βββββββ.β.βββββββββ.β.βββββββ.β.β.β.β.β
.β.β.β.....................β.β.β.............β.β.β.β.......β.......β...β.β.β.β...β...β...............β.........β.β.β.β.β
.β.β.βββ.βββββ.βββββββββββββ.β.βββββββββββββ.β.β.β.β.βββββ.βββββββ.βββ.β.β.β.βββββ.β.β.βββββββββββββββ.βββββββββ.β.β.β.β
.β.β.........β.β.....β.......β.β...........β.β.β.β.β.β...β.......β...β...β.β.......β.β.β.......β.....β.β.........β.β.β.β
.β.βββ.β.βββ.β.β.βββ.β.βββββ.β.βββββββββββ.β.β.β.β.β.β.β.β.βββββ.βββ.β.βββ.βββ.βββββ.β.β.βββββββ.βββ.β.β.βββββββββ.β.β.β
.β.β.β.β...β.β.β.β.β.β.β.....β...............β.β.β.β.β.β.β.β.......β.β...β...β.β.....β.β.......β.β.β.β.β.........β.β.β.β
.β.β.β.βββββ.β.β.β.β.β.βββ.β.βββββββββββββββ.β.β.β.β.β.βββ.βββββββββ.β.β.β.β.β.β.βββββ.βββββββ.β.β.β.β.β.βββββββ.βββ.β.β
.β.β.β.....β.β.β.β.β.β.....β.β...............β.β.β...β...β...........β.β.β.β.β.β...............β.β.β.β.β.......β.....β.β
.β.β.βββββ.β.βββ.β.β.βββββββ.β.βββββββββββββ.β.β.β.βββββ.βββββββββββββ.β.β.β.β.βββββββββββββββββ.β.β.β.βββββββββββββ.β.β
.β.β.....β.β.....β.β.β.....β.β.β...........β.β.β.β.β...β.....β...β.....β.β...β...................β.β.β.................β
.β.β.β.βββ.β.βββββ.β.β.βββ.β.β.β.βββ.βββββββ.β.β.β.β.β.β.βββ.β.β.βββ.βββ.β.βββββββββ.βββββββββββββ.β.βββββββββββββββ.βββ
.β.β.β.β...β.β.....β.β.β.β.β.β.β...β.........β.β.β.β.β.β.β.....β...β...β.β.........................β.β.....β.......β...β
.β.β.β.β.β.β.βββββ.β.β.β.β.β.β.βββββββββββββββ.β.β.β.β.β.βββββββββ.βββ.β.βββββββββββββββββββ.βββββββ.β.βββ.β.β.βββ.βββ.β
.β.β.β.β.β.........β.β.β...β.β.................β.β.β.β.β.........β...β.β.............β.......β.......β.β.............β.β
.β.β.βββ.βββββββββββ.β.βββββ.βββββββββββββββββββ.β.β.β.βββββββββ.β.β.β.βββββββββββββ.βββββββββ.βββββββ.βββββββββββββββ.β
...β.................β...........................β...β.................β.......................β.......................β
4456789012345678900999012345678901234β2109876543210987778901234567890β77890123456789012345678901234567890β5567890123455β
3βββββββββββββββββββ8βββββββββββββββ4β2βββββββββββββββ6β8β0β2βββββββββ6βββ0βββββββββββββββ6βββββββββ6β8β0β4βββββββββββ4β
22345678889012345β778β43210987655β3β5β3β334567890123456β9β1β334567890β655β1β09877β4321098777890β334β7β9β1β33456789012β3β
1βββββββ7βββββββ5β6βββ4βββββββββ4β2β6βββ2βββββββββββ4β6β0β2βββββββββ0βββ4β2β0βββ6β4β2βββββ8βββ0β2β4β8β0β2β2β4βββββββββ2β
001234567β1098766β5β9β5β55678β5β3β1β7β1123456789012β5β7β1β3β5567890β11234β3β1β87655β3β112β9β211β1β3β990β3β1β33456789012β
9βββββββββββββββββ4β8β6β4β6β8β4β2β0β8β0βββββββββββ2β6β8βββ2β4βββββ0βββββββ4β2βββββββ4β0βββ0β2βββ0β2βββββ4β0β2βββββββββ2β
8β99012345678901234β877β3β7β9β4321099901234567890β3β7β9901234β778β1β87655β5β3β0987655β099β1β3β099β11234β5β9β1β3345678β3β
7β8βββββββββββββββββ8βββ2β8β0β4β2β0β0βββββββββββ0β4β8βββββββββ6βββ2βββββ4β6β4β0βββββββββ8β2β4βββ8β0βββββ6β8β0β2βββββ8β4β
6β7β432109876543210999012β9β1β5β3β1β11234567890β1β5β990123456β654333456β3β7β5β1β432109877β3β5567890β556β77890123456β9β5β
5β6β4βββββββββββββββββββββ0β2β6β4βββββββββββββ0β2β6βββββββ4β6βββββββββ6β2β8β6β2β4βββββββ6β4βββββββββ4β6βββββββ2β4β6β0β6β
4β5β5β211123432109876543211β3β7β5567890123456β1β3β7β9987655β7789012β877β1β9β7β334β334β876555678901234β778901233β5β7β1β7β
3β4β6βββ0βββββ2βββββββββββββ4β8βββββββββββββ6β2β4β8β8βββββ6βββββββ2βββ8β0β0β8βββββ2β4β8βββββββββββββββ8βββββββββ6β8β2β8β
2β3β778901234β3β77890β0987655β9β09876543211β7β3β5β9β7β990β7789012β334β990β1β9999012β3β9β7789012β55655β9β432109877β9β3β9β
1β2βββ8β0βββ4β4β6βββ0β0βββββ6β0βββββββββββ0β8β4β6β0β6β8β0β8βββββ2βββ4β0βββ2βββ8βββββ2β0β6βββββββ4βββ4β0β4βββββββββ0β4β0β
0β1β3β9β112β5β5β5β3β1β1β09877β112345678901099β5β7β1β5β7β1β9β6543334β5β112β334β7β99012β1β6543211β3β7β3β1β556789011β1β5β1β
9β0β2β0βββββ6β6β4β2β2β2βββ8β8βββββββββββββββ0β6β8β2β4β6βββ0βββββββββ6β2β2β4β4β6β8βββββ2βββββββ0β2β6β2β2β6βββββββ0βββ6β2β
8β9β1β11234β7β7β3β1β3β21099β9β432109876543211β7β9β334β655β11234567877β3β3β5β5β5β876543334567890β1β5β1β3β7789012β09877β1β
7β8β0βββββ4β8βββ2β0β4βββββββ0β4βββββββββββββ2β8β0β4βββββ4βββββββββββββ4β4β6β6β4βββββββββββββββββ0β4β0β4βββββββββββββ8β0β
6β7β99012β5β99012β9β5β77890β1β5β65433345678β3β9β1β5β778β33211β877β87655β5β777β4321099901234567890β3β9β55678901234567890β
5β6β8β0βββ6β0βββββ8β6β6βββ0β2β6β6βββ2βββββββ4β0β2β6β6β8β2βββ0β8β6βββ6βββ6β8βββββββββ8βββββββββββββ2β8βββββββββββββββ8βββ
4β5β7β1β877β1β09877β5β5β5β1β3β7β778β210987655β1β3β7β5β9β1β21099β655β778β7β9901234567890123456789012β7β11234β7789012β990β
3β4β6β2β8β8β0βββββ6β4β4β4β2β4β8βββββββββββββββ2β4β8β4β0β0βββββββββ4βββ8β8βββββββββββββββββββ6βββββββ6β0βββ4β6β8βββ2βββ0β
2β3β5β3β9β990123456β3β3β433β5β87654321098765433β5β9β3β1β098765433β433β9β9901234567890β2109877β1123456β9β6555678901234β1β
1β2β4βββ0βββββββββββ2β2βββββ6βββββββββββββββββββ6β0β2β2βββββββββ2β4β2β0βββββββββββββ0βββββββββ0βββββββ8βββββββββββββββ2β
012β43211123456789012β210987654321098765432109877β112β33456789012343211β21098765432111234567890β43210987654321098765433β
.....................β.....β.....β...β...β...............β...β...β...β.......β...β...............β.......β.............β
.βββββ.βββββ.β.βββ.β.β.βββ.β.β.β.β.β.β.βββ.βββ.βββββββββ.β.β.β.β.β.β.β.βββ.β.β.β.βββ.βββ.βββ.βββ.βββ.βββ.β.βββββ.β.βββ.β
.β...β...β.........β.β...β.β.β.β.β.β.β.......β.....β.....β.β.β.β...β...β...β...β.......β...β.β.....β...β.β.β.....β.β...β
.β.β.βββββ.βββ.βββββ.βββ.β.β.β.β.β.β.βββ.βββββββββ.β.βββ.β.β.β.βββ.βββββ.βββββββββ.β.β.βββ.β.βββββ.βββββ.β.βββββ.β.βββββ
.β.β...β...β...β...β.....β.β.β.β...β...β.β.........β...β.β.β...........β.β...β.β...β.β.β.......β.β.β.....β.......β.....β
.β.βββ.β.β.βββββ.β.βββββββ.βββ.βββββββ.β.β.βββββββββ.β.β.β.βββββββ.β.βββ.β.β.β.β.βββ.β.β.βββββ.β.β.β.βββββββ.β.βββββββ.β
.β...β...β.β.....β...β...β...........β.β.β.......β...β.β.β.......β.β.β...β.β.β.β...β.β.β.β.....β...β.......β...β.....β.β
.βββ.βββββββ.βββββ.β.β.βββββββββββ.βββ.βββββββββ.β.βββ.β.β.βββββ.β.βββ.βββ.β.β.βββ.βββ.β.β.βββ.β.β.βββββ.βββββ.β.βββ.β.β
.β.β.........β...β.β.β.................β.........β.....β.β.β.....β.β...β...β.....β...β.....β...β...β...β.....β.β.β.....β
.β.βββββββββββ.β.βββ.β.βββββ.β.βββββ.β.β.β.βββ.βββ.βββ.β.βββ.βββββ.β.βββ.βββ.βββββ.β.βββββββ.βββββ.β.β.β.βββ.β.β.βββββ.β
.β.....β.......β.....β.....β.β.β.....β.........β...β.β.β...β...β.β...β.....β.β.....β...β.....β...β...β.β.β.β.β.β.......β
.βββ.β.β.βββββββββββββββββ.β.β.β.βββββββββ.βββ.β.β.β.β.βββ.βββ.β.βββββ.βββ.βββ.βββ.β.β.β.βββββ.β.βββββ.β.β.β.β.βββββββ.β
...β.β.β.β.............β...β.β.β...β.....β...β...β.β.....β.....β...β...β.β.β...β.β...β.β.......β.β.β...β.......β.β.....β
.β.β.β.β.βββββ.β.βββββ.βββββ.βββββ.βββ.β.β.β.β.βββ.βββββ.βββββββ.β.β.βββ.β.β.βββ.β.βββ.β.βββββββ.β.β.βββββββ.βββ.β.βββ.β
.β.β.β.β.β...β.β.β...β.β...β...........β.β.β.β.β.......β.........β.β.....β...β...β.....β.β...β...β.β.......β.....β.β...β
.β.β.β.β.β.β.β.β.β.βββ.β.β.βββ.βββ.βββββ.βββ.β.β.βββ.β.βββββββ.βββββββ.β.βββββββ.βββββ.βββ.β.β.βββ.βββββββ.β.βββ.β.βββ.β
...β.β.β...β.β...β...β.β.β.....β...β.........β.β.β...β.β...β...β.................β.........β...β...β.....β.β.....β.....β
.βββ.βββββββ.β.βββ.β.β.β.βββββ.β.βββββββββββββ.β.β.βββ.β.β.βββββ.βββββ.βββββββ.β.βββ.βββββββββββ.β.βββββ.β.β.β.βββββ.β.β
.β.....β.....β...β.....β...β...β.........β.β...β.β.β.β...β.......β.β...β.......β.......β...β.....β.......β.β.β.....β.β.β
.βββββ.β.βββββ.β.β.β.βββββ.β.βββββββββ.β.β.β.βββ.β.β.βββ.βββββββββ.β.βββ.βββ.β.βββ.βββ.β.β.β.β.β.βββββββ.β.βββ.βββ.βββ.β
.β.........β...β.β.β.......β...........β.β.β.β...β...β.......β.....β...β.β.β.β.β.β.β...β.β...β.β...β...β.β.........β...β
.β.βββββββ.β.βββ.β.β.βββββββββββββββ.β.β.β.β.β.βββββ.βββ.βββ.β.β.β.β.β.β.β.β.β.β.β.β.βββ.βββ.β.βββ.β.β.βββββββββ.βββ.β.β
...........β.β.β.β...β...β.....β.....β...β.β.β.β...β.....β.β.β.....β.β.β.β...β.β.β.β.β...β...β.β...β.β.β.......β...β.β.β
ββββ.βββββββ.β.β.βββββ.β.β.βββ.β.βββββββββ.β.β.β.β.βββββββ.β.β.βββ.β.β.β.β.βββ.β.β.β.β.β.βββββ.β.βββ.β.β.βββ.βββββ.β.β.β
.....β.........β.......β.....β...β...........β...β.............β.....β...β.....β.....β.β.......β.....β.....β.........β.β
990123456789012345678β77890β43334β112β334β876555654321099β778β556β990β3345433β099β099901234567890β0987655β9901099901234β
8βββββ4βββββ0β2βββ6β8β6βββ0β4β2β4β0β2β2βββ8βββ4βββββββββ8β6β8β4β6β8β0β2βββ4β2β0β8βββ8βββ2βββ6βββ0βββ8βββ4β8βββββ8β0βββ4β
7β099β556β211123456β9β655β1β5β1β5β9β1β2109990β43211β55678β5β9β3β777β112β655β211β8777890β334β7β21112β990β3β7β09877β1β655β
6β0β8βββββ2βββ2βββββ0βββ4β2β6β0β6β8β0βββ0βββββββββ0β4βββ8β4β0β2βββ6βββββ6βββββββββ6β8β0βββ4β6βββββ2βββββ2β6βββββ6β2βββββ
5β1β877β433β433β433β11234β3β7β9β778β099β1β334567890β334β9β3β11234567890β7β334β1β556β9β1β6555678β7β3β99012β6555556β33455β
4β2βββ6β4β4βββββ4β2βββββββ4βββ8βββββββ8β2β2βββββββββ2β4β0β2βββββββ6β8βββ6β2β4β0β4βββ0β2β6βββββ8β6β4β8βββββββ4β4βββββββ4β
3β334β655β5β87655β211β556β55678901234β7β3β2109877β112β3β1β2109877β7β9β556β1β5β9β433β1β3β7β99099β655β8765556β433β65433β3β
2βββ4βββββββ8βββββ2β0β4βββββββββββ2βββ6βββββββββ6β0βββ2β2β2βββββ6β8βββ4βββ0β6β8βββ2βββ4β6β8βββ0β6β6βββββ4βββββ2β6βββ2β2β
1β1β554321099β334β3β9β43210987654333456β990123456β99012β3β3β33456β9β334β990β77890β112β55678β211β777β112β33211β1β5β43211β
0β0βββββββββββ2β4βββ8β4βββββ8β6βββββ4β6β8β0βββ4βββ8βββ2β4βββ2βββββ0β2βββ8βββ8βββββ0β2βββββββ2βββββ8β0β2β2βββ0β0β4βββββ0β
9β09990β7789012β55678β55678β9β7β87655β778901234β778β7β3β556β211β7β112β77890β9β77890β333β99012β211β990β3β1β9β9β9β4321099β
8βββ8β0β6βββββββββββββββββ8β0β8β8βββββββββ0βββ4β6β8β6β4βββ6βββ0β6βββββ6βββ0βββ6βββ0β2β2β8βββββ2β0βββββ4β0β8β8β8βββββββ8β
777β7β1β5β8765432109877β099β1β9β990β65556β112β556β9β65556β77890β556β556β5β1β556β5β112β1β8765433β9β7β655β0987778β7β99877β
6β6β6β2β4βββββ4β2βββββ6βββββ2βββββ0βββ4β6β2β2β6βββ0βββββ6βββββββ4β6β4βββ4β2β4βββ4β2βββ0β8βββββββ8β6β6βββββββ6βββ6β8βββ6β
5β5β5β3β3β099β5β3β556β5β099β33432111234β7β3β3β7β2111234β778901234β7β43334β334β433β21099β9β334β778β5β7789012β55655β7β655β
4β4β4β4β2β0β8β6β4β4βββ4β0β8βββ4βββ2βββββ8βββ4β8β2βββ2β4βββββββ2βββββββ2β4βββββββ2βββββ8βββ2β4β6βββ4βββββββ2β4βββ4β6βββ4β
334β3β5β211β7β655β333β3β1β87655β433β210987655β9β3β433β5β990β433β77890123456789012β877789012β556β334β43211β3β43334β65433β
2βββ2βββββββ6β6βββ2β2β2β2βββββ6β4βββββββββββββ0β4β4βββ6β8β0βββββ6βββββ2βββββββ0β2βββ6βββββββββββ2β4βββββ0β4β4β2βββββ4β2β
1β43211β33456β777β21112β334β877β556789012β3β211β5β5β9β778β1123456β1β433β5543211β3345678β877β43211β5567890β5β5β11234β5β1β
0βββββ0β2βββββ8β6β2β0βββββ4β8βββββββββ0β2β2β2βββ6β6β8βββ8βββββββββ0β4βββ4βββ2β2βββ4βββ8β8β6β4β2β0βββββββ0β6βββ0βββ4βββ0β
9β778901211β099β5β3β0987655β99012343211β3β1β3β877β778β0999012β99990β556β3β7β3β3β3β5β099β9β655β3β099β099β1β778901234β990β
8β6βββββββ0β0βββ4β2β0βββββββββββββββ2β2β4β0β4β8βββββ8βββ0βββ2β8β8β0β4β6β2β6β4β4β2β6β0βββ0βββ6β4βββ8β0β8βββββββββ2βββ8β0β
87655567890β1β5β3β211β655β21099β65433β334β9β5β9β334β99011β7β3β77890β3β7β1β655β5β1β7β1β211β877β5β778β1β7β4321112β334β7β1β
ββββ4βββββββ2β4β2βββββ6β4β2βββ8β6βββββββββ8β6β0β2β4βββββββ6β4β6βββ0β2β8β0β6βββ6β0β8β0β2β2βββββ6β6βββ2β6β4βββ0βββββ4β6β2β
01234β876543334β2109877β43334β877β65432109877β112β5567898765556β21112β990β77877β09990β3β2109877β65433β65556β098765556β3β
...............β.......β.........β.................β.......β.......β.......β.............β.β.........................β.β
.βββββββββββββββ.βββ.β.βββ.β.β.β.βββββββββββ.β.βββ.βββββ.β.βββββ.β.βββββ.β.βββ.β.β.βββββ.β.β.βββ.β.β.βββββ.β.βββ.β.β.β.β
...β...β.....β...β...β.....β.β.β...β...β.....β...β...β...β...β...β...β...β...β.β.....β.........β...β...β...β...β...β...β
.β.β.β.β.βββ.β.βββ.βββββββββ.β.β.β.β.β.β.β.βββ.β.βββ.β.βββ.β.β.βββββ.β.βββββ.βββ.βββ.β.βββββββ.β.βββββ.β.βββββ.βββ.βββ.β
.β...β...β...β...β...β.β.......β.β...β...β...β.β...β...β.....β.....β...β...β.....β...β...β...β...β...β.......β.....β...β
.βββββββββ.βββ.β.βββ.β.β.βββ.βββ.βββββββββββ.βββββ.βββββ.β.βββββ.β.β.βββ.β.βββββββ.βββββ.β.β.β.βββββ.βββββ.β.β.βββββ.β.β
.β...β...β...β...β.......β.....β.........β...β.........β...β...β.β...β...β...β...β.....β...β...β.......β...β.......β...β
.β.β.β.β.βββ.β.βββ.βββ.βββ.βββ.β.β.βββββ.β.βββ.βββββ.β.β.βββ.β.βββββ.β.β.βββ.β.β.β.βββ.β.βββββββ.β.βββ.β.βββββ.β.βββββ.β
...β...β...β.β...β...β.β...β.....β...........β...β...β.......β...β...β...β.β...β.β.β.β...........β.β...β...β.β.β.β.....β
.βββββββ.β.β.βββ.βββ.βββ.βββ.β.βββ.βββ.β.βββ.β.β.β.βββββββ.β.βββ.β.βββ.β.β.βββββ.β.β.βββ.βββ.βββββ.β.βββββ.β.β.βββ.βββββ
...β.....β...β...β...β...β...β.β...β...β...β.β...β...β.β.......β...β.........β...β...β.β...β...β...β...β.β...β...β...β.β
ββ.β.βββ.βββββ.βββ.βββ.βββ.βββ.β.βββ.βββ.β.β.β.βββββ.β.β.βββββ.βββββββ.βββββ.β.βββββ.β.βββ.βββ.β.βββββ.β.βββ.βββ.βββ.β.β
...β...β...β...β.β...β...β...β.....β...β...β.......β.β...β...β.......β.....β.β.β.β.....β...β.β...β.β.....β...β...β.β...β
.βββββ.βββ.β.βββ.βββ.βββ.βββ.βββββ.βββ.β.βββββ.βββ.β.βββ.βββ.βββ.β.β.β.βββ.β.β.β.β.βββββ.βββ.βββββ.β.βββββ.βββ.β.β.βββ.β
...β...β...β...β...β.......β...β...β...β...β...β...β...β.β.....β...β.β.....β.β...β...β...β.β.....β...β.....β.........β.β
ββ.β.βββ.βββββ.β.β.βββββββββββ.β.βββ.βββββ.βββββ.βββββ.β.β.βββ.βββββ.β.βββββ.βββ.βββ.β.βββ.β.β.βββ.βββ.βββββ.βββββββββ.β
...β.β.β...β.....β.....β...β...β...β...β.......β.......β.....β.......β...β...β...β...β...β.β.β...β.β...........β...β...β
.βββ.β.βββ.β.βββββββ.βββ.β.β.βββββ.βββ.βββββββ.β.β.βββββββββ.β.β.β.βββββ.β.β.β.βββ.βββββ.β.β.βββ.β.β.βββ.β.βββ.β.β.β.βββ
.β...β...β...β...β...β...β...β...β...β...β...β...β...β.....β...β...β.....β.β.β.β.β...β...β.....β...β...β.β...β...β...β.β
.β.βββ.β.βββββ.β.βββββ.βββββββ.βββββ.βββ.β.β.βββ.βββ.βββ.β.β.βββ.βββ.βββββ.βββ.β.βββ.β.βββ.βββββ.βββββ.β.β.β.βββββββββ.β
.......β...β...β.....β...β.....β...β...β...β...β...β...β.β.........β...β.β.......β...β...β.β...β...β...β.β...β...β.....β
.βββββββββββ.βββββββ.βββ.β.βββββ.β.βββ.βββββββ.β.β.β.β.βββββββββββ.β.β.β.β.βββββββ.βββββ.βββ.β.β.βββ.βββββ.β.β.β.βββββ.β
.β.....β.....β...β...β...β...β...β...β...β.......β.β...β.....β...β.β.β.β...β...β...β...β...β.β.......β.....β...β...β...β
ββ.βββ.β.βββββ.β.β.βββ.βββ.β.β.βββββ.β.β.β.β.βββ.βββ.βββ.βββ.β.β.β.β.β.βββββ.β.β.βββ.β.βββ.β.βββ.β.βββ.βββββββββββ.β.β.β
...β.....β.....β.......β...β...............β...β.........β.........β.β.......β.......β...β.....β.....................β.β
778901234567890β4321099β654333334β43210987655567890β0987778β0987655β8765556β8765555567890β3β5567890123456789012345678β1β
6βββββββββββββββ4βββ0β8βββ4β2β2β4βββββββββββ4β6βββ0βββββ6β8βββββ6β4βββββ4β6βββ6β4β4βββββ0β2β4βββ8β0β2βββββ8β0βββ4β6β8β0β
556β877β43211β655β990β87655β1β1β334β778β11234β778β112β556β990β556β433β334β778β7β33456β211123456β990β334β778β112β556β990β
4β6β8β6β4βββ0β6βββ8βββββββββ0β0β2β4β6β8β0β2βββ8β8βββ2β4βββ0β0β4βββββ2β2βββββ8βββ2βββ6β2βββββββ6β8βββββ4β6βββββ2βββ6βββ0β
3β778β655β990β778β877β5β2109990β1β556β990β211β9β990β334β99990β43211β112β778β99012β877β334β778β778β877β5567890β33456β211β
2βββββββββ8βββ8β8βββ6β4β2βββ8βββ0βββββββββββ0βββββ0βββββ8β8βββββ2β0β0βββ6β8βββββββ8βββββ4β6β8β8βββββ6βββββ8β0β2βββββ2β2β
1β877β433β877β999β8765433β09877β990123456β990β990111234β778β112β3β099β555β990β334β99012β556β999β3345678β099β1123456β333β
0β8β6β4β2βββ6β0βββ8βββ4βββ0βββ6β8β0βββββ6β8βββ8βββββ2β4β6βββ0β2βββββ8β4β4βββ0β2β4β0βββ2β4βββββββ2β4βββ8β0βββββ2β4βββββ4β
099β655β112β5β112β990β5β211β55678β11234567890β777β433β5567890β334β778β333β5β112β5β1β5β33456789012β5β099β112β5β3β5β87655β
0βββββββ0β2β4βββ2βββ0βββ2βββ4β6βββ2βββ4β6βββ0β6β6β4βββββββ8β0βββ4β6βββ2β2β4βββββ6β2β4βββ4βββ8βββββ4β0βββββ2β4β4βββ8βββββ
112β21099β334β433β211β433β334β7β112β655β778β1β556β556β3β0999012β556β211123456β877β334β7β556β990β334β112β5β334β556β990β3β
ββ2β2βββ8βββββ4βββ2βββ4βββ2βββ8β0βββ6βββ8β8β2β4βββββ6β2β0βββββ2βββββββ0βββββ6β8βββββ4β6βββ6βββ0β2βββββ2β4βββ4βββ6βββ0β2β
433β334β877β655β3β211β556β211β99012β778β999β3345678β7β211β099β3345678β99011β7β9β3β65556β877β5β112β7β43334β655β877β1β112β
4βββββ4βββ6β6βββ2βββ0βββ6βββ0βββββ2βββ8β0βββββ4βββ8β6βββ2βββ8βββ4β6β8β8βββ0β8β0β2β6βββββ8βββ4βββββ6β4βββββ6βββ8β8β0βββ2β
556β655β556β778β112β0987778β099β433β099β112β655β099β655β3β77890β556β9β77890β9β112β778β099β1β43334β655β09877β099999012β1β
ββ6β6βββ4βββββ8β0β2βββββββββββ8β4βββ0βββββ2βββββ0βββββ4β4β6βββ0βββββ0β6βββββ0βββ2βββ8β0βββ0β4β2βββ6βββ0βββββ0βββββββββ0β
556β7β7β433β09990β33456β334β778β556β112β4333456β9901234β55678β1123211β655β211β112β099β112β9β5β211β7β21112321112β556β990β
4βββ8β6βββ2β0βββββββ4βββ2β4β6βββββ6βββ2βββββββ6β8β0βββββββββ8β0β2β2βββββ4β2β2β0βββ0βββββ2β8β6βββ0β8β2βββ2β2βββ2β4β6β8βββ
3β099β556β211β556β655β112β556β990β778β334β778β778β112β65433β990β333β11234β3β3β9β1β112β433β87778β999β333β3β334β334β778β9β
2β0βββ4β6βββββ4β6βββββ0βββββββ8βββββ8βββ4β6β8βββ8βββ2βββ4β2β0βββ2βββ0βββββ4βββ8β0βββ2β4βββ8βββββ8βββββ2β4β4β4βββββββββ8β
2111234β778β334β77890β099β55678β877β990β556β999β990β334β5β211123211β099β9β5567890β433β556β9β334β778β112β5β555β990β09877β
2βββββββββββ2βββββββ0βββ8β4βββββ8β6βββ0βββββββ8β0β0β4β4βββββββββββ0β0β8β8β6βββββββ4βββββ6βββ2β4β6βββ0βββββ6β6β8β0βββββ6β
3β33456β99012β655β211β778β433β099β655β112β5567890β1β555β99012β556β9β1β7β877β099β655β112β778β1β5567890β33456β778β112β556β
ββ2βββ6β8βββββ6β4β2βββ6βββ4β2β0βββββ4β2β2β4β6βββ0βββ4βββ8βββ2β4β6β8β2β6βββββ0β8β6βββ0β2βββ8β0βββ6β8βββ2βββββββββββ2β4β6β
012β87778β09877β4333456β655β210987654333334β778β112345678β433345678β3β6543211β8777890β334β99012β778901234567890123334β7β