Code challenge accepted
Hi all,
One of my kids dared me to code a game. A game I used to play a long time ago. The game was named "Empire" by Walter Bright. It was the "War game of the century". If you don't believe me...It says so on the box ;-). And yes I only could play the CGA four color version.
So I took up the challenge and I will try to recreate it. Of course, I want to learn from it as well. So these are my rules of engagement (okay, the war slang is getting to my head already):
- Cost of operation and development should be less than 100$ a year
- Make use of AI for graphic design
- Use cloud hosting for all components
- Write about it(this page)
- Show the development steps
- Get it in the app stores of Android and iPhone
Step 1 - WorldMap
First, we focus on the most prominent part of the game a view of the world map. It needs to be fast and only render the visual part. Since I'm going for a web stack I decided to make it a table. The trick here is to update the cells smart and to also allow for scrolling the table for half a cell.
I added ReactJS and Webpack as development frameworks for now I'm sticking to plan vanilla javascript. As a back-end server, I'm going to use NodeJS. This will allow me to reuse the code from the front end for the back end as well. The game state including the map is hosted by NodeJS using a MongoDB for storage. Using their free tier. So no cost added. I'm using GitHub as my cloud git repository (rslijp/soft-war). I made the repository public and feel free to track my progress
You see here a viewport of the map with land(L), sea(S), mountains(M) and cities(C). The graphics will be improved after I'm able to deploy
- Time:
- 6h
- Cost:
- 0$
- Stack:
-
- NodeJS
- WebPack
- ReactJS
- Cloud:
Step 2 - Deploy
Now we have some parts running, we need to deploy the application(and this site). For this, I'm looking at Heroku. They of a 5$ a month plan. This should keep me well within my budget. They offer a command line CLI which will deploy the application using a git repository. So whatever is in the git repository it will be deployed. To make this work I'm bundling my front-end and back-end as one package for now I do this with a simple shell script. You can create an Heroku account online. After that in a new folder it is relatively easy to host your app.
npm install -g heroku
heroku login
heroku create soft-war
git commit -am "Initial commit"
git push heroku main
heroku logs --tail
Secret are injected in the environment (same as most modern docker-based, kubernetes environments). Injecting a secret is done with the same cli. For example the database url to MongoDB:
heroku config:set ATLAS_URI=mongodb+srv://renzoslijp:<YOUR_SECRET_PASSWORD>@clustersoftwar.i5zrm6h.mongodb.net/?retryWrites=true&w=majority
After updating an environment variable the application redeploys automatically. When you want to update your application the following steps are enough. The beauty of using GIT as a delivery mechanism is that versioning and effective file storage are build-in. So updating the application required the following steps:
git commit -am "My new feature"
git push heroku main
heroku logs --tail
Deploying is quite easy after you get the kinks out. You get a pretty ugly URL https://soft-war-009a7cfaebd9.herokuapp.com/ and that is not handy and nice. My regular DNS entries are managed with Trans IP. So I linked my application to the actual Heroku deployed app under https://game.softcause.com. I used a subdomain an CNAME to do this. There were no additional costs involved.
Last I enabled SSL. Heroku offers that feature for free within the bundle of 5$. So I enabled it. Unfortunately, they don't offer an option for automated redirection to SSL. So I had to throw in a few lines of code for that. But I got that sorted now as well. Done for the day.
- Time:
- 9h (+3h)
- Cost:
- 60$ (a year)
- Stack:
-
- NodeJS
- WebPack
- ReactJS
- Heroku
- Cloud:
Step 3 - More work on the world map
Yes, I updated the styling of this page a bit. Let me know what you think of it. I also made the viewport react to the screen size updates. And..you can now drag it with the mouse. A more complex bit was getting the horizontal and vertical axis legends in. They scroll similar but different than the full map. It involved quite a bit of coding. The next big job would be to make the land look nice and not as plain blocks.
I think the viewport is okay. It still needs some extra styling on the scroll bars, but other than that. I'm happy. I do regret making the world cyclic and to allow to scroll partial cells in to the viewport. What a pain that creates...with scroll bars and so. :'-D
- Time:
- 12h (+3h)
- Cost:
- 60$ (a year)
- Stack:
-
- NodeJS
- WebPack
- ReactJS
- Heroku
- Cloud:
Step 4 - Make the map (more) pretty
So finally I have time to make the map look less like a a kid playing with bricks. I found a tileset generated by a co worker back(Rudy Bruning) in 2013. And I decided to use that one. I don't have a better one at this point and to be honest. I tried generating it with the AI DALL-E and that became an epic failure. The map was stored in the database in a rather big structure so first I put it on a diet. It is now stored as follows. In the previous storage each cell looked like
{"type": 'L', "detail":'grass'}. You
can see how many times "type" and "detail" would be repeated and create a lot of data traffic.
Before I started refactoring I added a git workflow: ES Lint check. This will create a pipeline on git you can view here. For now it only checks my styling, but unit test will be added when generating models
"world" : [
"LLLLLLSSSSSLLLLLLLLLLSSSSSSLLLLL",
"MMLLLLSSSSSSLLLLMMLLLLSSSSSSLLLL",
"MMMMLLLLSSSSSLLLMMMMLLLLSSSSSLLL",
"MMMLLLLSSSSSLLLLMMMLLLSSSSSSLLLL",
"MMMLLLLSSSSSLLLLMMMLLLSSSSSSLLLL",
"MMLLLLLSSSSSLLLLMMMLLLSSSSSSLLLL",
"LSSSSSSSSSSSSSLSLSSSSSSSSSSSSSLS",
"LSSSSSSSSSSSSSLSLSSSSSSSSSSSSSLS",
"LSSSSSSSSSSSSSLSLSSSSSSSSSSSSSLS",
"SSLLLLLLLLSSSSSSSSLLLLLLLLSSSSSS",
"SSLLLLLLLLSSSSSSSSLLLLLLLLSSSSSS",
"LLLLLLLLLLSSSSSSLLLLLLLLLLSSSSSS",
"LLLLLLLLLLLLSSSSLLLLLLLLLLLLSSSS",
"LLLLLLLLLLLLLSSSLLLLLLLLLLLLLSSS",
"MMMMMMLLLLLLLLLSMMMMMMLLLLLLLLLS",
"MMMMMMMMMLMLMMLLMMMMMMMMMLMLMMLL",
"LLLLLSSSSSSLLLLLLLLLLSSSSSSLLLLL",
"MMLLLLSSSSSSLLLLMMLLLLSSSSSSLLLL",
"MMMMLLLLSSSSSLLLMMMMLLLLSSSSSLLL",
"MMMLLLSSSSSSLLLLMMMLLLSSSSSSLLLL",
"MMMLLLSSSSSSLLLLMMMLLLSSSSSSLLLL",
"MMMLLLSSSSSSLLLLMMMLLLSSSSSSLLLL",
"LSSSSSSSSSSSSSLSLSSSSSSSSSSSSSLS",
"LSSSSSSSSSSSSSLSLSSSSSSSSSSSSSLS",
"LSSSSSSSSSSSSSLSLSSSSSSSSSSSSSLS",
"SSLLLLLLLLSSSSSSSSLLLLLLLLSSSSSS",
"SSLLLLLLLLSSSSSSSSLLLLLLLLSSSSSS",
"LLLLLLLLLLSSSSSSLLLLLLLLLLSSSSSS",
"LLLLLLLLLLLLSSSSLLLLLLLLLLLLSSSS",
"LLLLLLLLLLLLLSSSLLLLLLLLLLLLLSSS",
"MMMMMMLLLLLLLLLSMMMMMMLLLLLLLLLS",
"MMMMMMMMMLMLMMLLMMMMMMMMMLMLMMLL"
]
This is the hand made map you see on the game page. To make this map look pretty. After loading it from the datbase the old structure is still inflated. The biggest step is to get the detail field set correctly. This determines the look of a tile. I want to do the following steps
- Apply default tile styling (with some variation)
- Add some landscape transitions using pattern matching
- Generate random new maps, with a growth algorithm
Step one was pretty straight forward. And the map now looks like this
As you can see the pattern repetition is pretty visible. Hopefully step 2 will sort this out.
By making a pattern of 3x3 or 4x4 we can sweep the map and look for transitions and then update one ore more tiles. For example creating a shore, could be detected with the following pattern. And then we can change the center tile. The sweep should take care of the adjecing tiles.
So let's put this to the test
To keep the number of tiles under control. I make it a bit easy for myself. For example no mountains near the sea.
Making the transitions is quite tedious job to be honest, but slowly the shore lines are getting there. I think I do need to make so variations on the long shores, and fiddle witht the corner tiles to make it look okay. And then next some green field variations with combined mountains, lakes and dry lands...may be a bit of now. Anyway my progress so far:
Below here you can see the final map and for now I'm happy with my map tile set. It has some variations and it looks like all transistions are there.
Moving to the next step generation of maps...
Step three - A generating a map. This is actually quite easy. We first make an empty canvas in this case filled with sea (S)
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | ||||||||
| 3 | ||||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | ||||||||
| 8 |
Step three - B Now we drop some random land slots. We use a seed-factor for this. We pick a random
number between 0 and 1 and if that number is below 0.008 we marked that piece of sea as
Land (L).
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | L | |||||||
| 3 | ||||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | L | |||||||
| 8 |
Step three - C In a couple of cycles we are growing our land mass. For a regular land configuration we use 5 cycles and a cycle consists of a swoop over the whole map. We want so randomization here as well. So we use a matrix which
doesn't promote equal growth. The center element with factor 1.0 is the land tile we investigate. With factor 1.0 we never loose previously generated land.
const matrix = [
[0.6,0.8,0,6],
[0.8,1.0,0,8],
[0.6,0.8,0,6]
];
Cylce 1
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | L | L | ||||||
| 2 | L | L | ||||||
| 3 | L | L | ||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | L | L | ||||||
| 7 | L | L | ||||||
| 8 | L |
Cylce 2 (notice the map is cyclic
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | L | L | L | L | ||||
| 2 | L | L | ||||||
| 3 | L | L | ||||||
| 4 | L | |||||||
| 5 | L | L | L | |||||
| 6 | L | L | ||||||
| 7 | L | L | ||||||
| 8 | L | L | L |
Step three - D In one sweep we generate mountains. For a mountain to be created we need a 3x3 grid of
land only. Once a mountain is generated the change for a joining mountain is higher. We start with a seed factor of
0.2. For each mountain in that grid we add a 0.1 to the change factor.
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | M | M | ||||||
| 3 | ||||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | M | |||||||
| 8 |
Step three - E Cities are generated. On a map with dimensions width x weight we use the
formulate (width + weight) / 2. Cities can be created on land. Cities repel each other. Mountains also repel cities,
but land gives a small bonus. Water gives even more bonus; so there is a likelyhood that cities are placed near water.
Since we use change here as well. It is not garantueed that the cities are all near water. Or tht each 'continent' has the
same amount of cities. For now I consider this 'part of life'.
I will allow the user to select 3 types of maps. 'REGULAR', 'CONTINENTS' and 'ISLANDS'. I can achieve this by playing with the initial seed factor(Step three - B), cycles(Step three - C) and grow factor mountains(Step three - D). These are the values I use.
| seed | cycle | mountain | |
|---|---|---|---|
| REGULAR | 0.008 | 5 | 0.2 |
| CONTINENTS | 0.004 | 7 | 0.15 |
| ISLANDS | 0.016 | 3 | 0.3 |
As a final tweak, I put the whole map in a sprite-map. This is one big image containing all land tiles in one. This optimizes the loading time by a huge factor. Now only one image is retrieved instead of 72 images. You can see the sprite map here. You then can select the part you need with css. I generated the sprite map online with this site .
And I reverted the sprite map. The technique works, but this has to be part of the webpack build. Not by hand.
- Time:
- 19h (+7h)
- Cost:
- 60$ (a year)
- Stack:
-
- NodeJS
- WebPack
- ReactJS
- Heroku
- Cloud:
Step 5 - Users and gamestate
Next step we will focus on logging in with oAuth2 (gmail). And we retain states for that user, but tht is for tomorrow.
Using a the passport module for NodeJS. Adding oAuth becomes very easy. You can read more on this module
here. After registering the client id, client secret, and callback url.
We are in business. The final step is to protect the end-points of our non public pages. We do this by adding
express middleware in nodejs on the end-points.
This concept is slightly different then my typical .NET MVC or Spring-boot applications, but the concept is
relatively simple. In express you define routes to end points and they can be routed to either end-points or
or sub routes. This allows to creating simpke hierachies. For now I consider all with /,
/index.html and all end-points that starts with /api private. Private end-points
require a logged in user.
The user-session is stored in an in-memory session on the server side.
To make up for some time in the UI. I'm going to use what I know and I'm going to use react bootstrap to get a quick start. React bootstrap is a react library on top of the well known bootstrap UI/Layout framework
So we also added some routing in the application now. Which allows for creating a new game or continueing and old game. Next step is inviting your friends and surrendering a game.
I'm using Send grid to sent my mails. I use this rather than hooking it up directly to gmail. Because gmail limit usage to only 300 different email addresses day. This could mean I can't mail after 300 new users subscribe. For now Send grid has a verification phase. I got that sorted and their API's work pretty straight forward. So we have mail. To be honest all the mail traffic is time consuming. So I park the fancy layout for later.
- Time:
- 29h (+8h)
- Cost:
- 60$ (a year)
- Stack:
-
- NodeJS
- WebPack
- ReactJS
- Heroku
- Cloud:
Step 6 - Adding game logic
Now we can start adding same game logic. I start directly with the AI as well. We are going to use a javascript game model. Which we will also test with Jasmine-
For the game model we are going to use traits. Traits are mixed in. This is different than Object
oriented development. I decided to expirement a bit with this. It means an object can identify itself
with multiplpe traits. You mix them in. In this way you can compose an oject. For comparison
in normail OO. How would you model a flying car? Does it extend from Car? Or Plane?
With traits your vehicle will mixin the Plane and the Car traits. And be happy.
Another expirement is to heavily use a message bus for loosely linking all the components. WIth this I hope to reduce the internal dependencies and method calls back and forth. So let's say. If a according to the game-map and unit movement points a tank is allowed to move. A message will be broadcasted to move the tank. All interested parties can update their state:
- Movement the tank
- Movement points deducted
- Update the fog of war(ow yes we need to make that)
- Deferred Rerender
- And what not more...
So to make it .... I like to add same graphics and I asked DALL-E of the chat GPT famliy to render my units. This set me back for 20$. It is a one time investment. I have to say... it was interesting. God...AI's can be stubborn when the model is not fitting.
But besides some struggle all in all..I'm happy. Let me present you all the units:
Infantry |
Tank |
Truck |
Fighter |
Bomber |
Helicopter |
Destroyer |
Cruiser |
Battleship |
Aircraft carrier |
Transport |
Submarine |
Missile |
City |
Explosion |
To get a skeleton in place for the game logic. I added a simple tank and some basic logic. You can try it. You can use the arrow on screen, the arrow keys or the regular keys (QWEADZXC) to move the tank.
| Q left-up |
W up |
E right-up |
|||||
| A left |
D right |
||||||
| Z left-down |
X down |
C right-down |
|||||
| space next unit |
|||||||
Currently figuring out how to include the Jasmine spec runner in the game. It is quite easy to install and get running. It also comes with support for most IDEs so working with it from IntelliJ is quite fast
Testing is fast. And test are written pretty easy:
it("should allow movement on land for a Infantry", function(){
//Given
var unitInstance = new unit("I", {x: 4, y:8});
//When
var result = unitInstance.canMoveOn({type:"L"});
expect(result).toEqual(true);
});
This setup lets me write unit test fast. Already at 223 tests. Whoohaa. It also allows for TDD
(Test Driven Development) but it is a
bit clumpsy in that regards. Syntax errors make it hard to figure out where it breaks. So
mostly using TAD (Test After Development). To keep up we do small build test cycles. No big bangs.
I gave it some thoughts how to proceed and I decided to do the next hard thing. Besides developing the game logic and front-end. Which would be adding more dialogs and so. I'm going to work on the AI this would mean the multiplayer part wouldn't work until it is finished. But it is quickest. For fun I will allow to see how the AI thinks on screen. More on that in step 7.
- Time:
- 43h (+14h)
- Cost:
- 60$ (a year) + 20$ (one time)
- Stack:
-
- NodeJS
- WebPack
- ReactJS
- Heroku
- Jasmine
- Cloud:
- Tools:
Step 7 - Making an AI
So to make an AI we need our majority of gamelogic in place. So I focussed on that. Reloading the page each time is too tedious. So as described in Step 6 we use Jasmine. At this point the majority of the game logic is in place. And we have 427 tests cases. WHoohooo
But All in all it is time consuming it did take a whole day. Mainly the automated navigation was a bit complex. I Decided to go with the A* navigation algorithm. I used this algorithm before. It works in a similar manner as the GPS algorithm. It stands for Generic Problem Solver. I do have a weak spot for this type of alogorithm. Where normal recursion is depth first and uses a back tracking algotihm. Like the bread crumbs of "Tom Thumb". This is a broad first style algorithm. It does consume more memory. Let me show (GPS not A*). We assume we do have a flat earth (no pun intended):
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | ||||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
So we need to find a path from S to E. In a regular back track algorithm. You would start and follow a random path and hope for the best. If you hit a dead end. You go back. But you do leave crumbs
1st Solution - Depth first/back tracking naive. We use a random direction
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | |||||||
| 2 | S | |||||||
| 3 | ||||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 2 | 1 | ||||||
| 2 | S | |||||||
| 3 | ||||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | - | - | ||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | - | - | ||||||
| 2 | S | |||||||
| 3 | 1 | 2 | ||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | - | - | ||||||
| 2 | S | |||||||
| 3 | 1 | 2 | ||||||
| 4 | 3 | |||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | - | - | ||||||
| 2 | S | |||||||
| 3 | 1 | 2 | ||||||
| 4 | 3 | 4 | ||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
...This will continue until we reached our end point. Or not.
As you can see choosing a random path. Will not automatically get the shortest route. Also calculcation in the form of permutations will increase a lot. We are just trying something. One of the fixes is to when you have a route. Try to optmize it. But then the damage is already done. You can see the not optimal tracks highlighted below. Both orange locations can also be reachted in 2 steps. A better way is to keep track of the "cost" of a path
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | - | - | ||||||
| 2 | S | |||||||
| 3 | 1 | 2 | ||||||
| 4 | 3 | 4 | ||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
2nd solution - Broad first/GPS without optimization
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | 2 | 2 | 2 | |||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 3 | 1 | 1 | |||||
| 4 | 3 | 2 | 2 | 2 | ||||
| 5 | 3 | 3 | 3 | 3 | ||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 3 | 1 | 1 | |||||
| 4 | 3 | 2 | 2 | 2 | ||||
| 5 | 3 | 3 | 3 | 3 | 4 | |||
| 6 | 4 | 4 | 4 | 4 | ||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 3 | 1 | 1 | |||||
| 4 | 3 | 2 | 2 | 2 | ||||
| 5 | 3 | 3 | 3 | 3 | 4 | |||
| 6 | 4 | 4 | 4 | 4 | ||||
| 7 | 5 | 5 | 5 | 5 | E | |||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 3 | 1 | 1 | |||||
| 4 | 3 | 2 | 2 | 2 | ||||
| 5 | 3 | 3 | 3 | 3 | 4 | |||
| 6 | 4 | 4 | 4 | 4 | ||||
| 7 | 5 | 5 | 5 | 5 | E | |||
| 8 | 6 | 6 | 6 | 6 | 6 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 3 | 1 | 1 | |||||
| 4 | 3 | 2 | 2 | 2 | ||||
| 5 | 3 | 3 | 3 | 3 | 4 | |||
| 6 | 4 | 4 | 4 | 4 | ||||
| 7 | 5 | 5 | 5 | 5 | E | |||
| 8 | 6 | 6 | 6 | 6 | 6 | 7 |
Notice that we now have a away of detecting that we have a better path. To our disposal. The only questions is when do we do this check. This is a good moment to look at the GPS approach. This version doesn't do any optimization
3rd Solution - Depth first/back tracking with early pruning
We use the direction that closest dx, dy to our goal.
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | |||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | 4 | ||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | 4 | - | |||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | - | - | |||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | - | - | |||||
| 6 | 4 | |||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | - | - | |||||
| 6 | 4 | |||||||
| 7 | 5 | E | ||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | - | - | |||||
| 6 | 4 | |||||||
| 7 | 5 | E | ||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | - | - | |||||
| 6 | 4 | |||||||
| 7 | 5 | E | ||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | - | - | |||||
| 6 | 4 | |||||||
| 7 | 5 | E | ||||||
| 8 | 7 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | |||||||
| 5 | 3 | - | - | |||||
| 6 | 4 | |||||||
| 7 | 5 | E | ||||||
| 8 | 6 | 7 |
At a first glance we did 5 more cycles but with less calculations. We used a simple pruning algorithm which is cost based on the remaining distance. This good be our winner. However what if the detour at 5,5 and 5,6 was bigger. This algorithm would just keep following it.
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | ||||||||
| 2 | S | |||||||
| 3 | 1 | |||||||
| 4 | 2 | - | ||||||
| 5 | 3 | - | - | - | - | |||
| 6 | 4 | |||||||
| 7 | 5 | E | ||||||
| 8 | 6 | 7 |
The final step is to combine them together. That is basically A*. The cost algorithm described in A* I will(/can) not explain.
Final solution - Broad first and depth first combined.
We make the combination by adding a priority to our head in broad first. If a candidate ranks lower.
In priority we will not calculate it in this turn and just keep it open as an option (orange).
I hope it is clear that the heads we continue with are the heads with the lowest distance to the target with the minimal amount of moves.
We we open a head. We need to remember our parent. So a 3 knows from which 2 step it came.
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | ||||||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | 2 | 2 | 2 | |||||
| 5 | ||||||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | 2 | 2 | 2 | |||||
| 5 | 3 | 3 | 3 | |||||
| 6 | ||||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | 2 | 2 | 2 | |||||
| 5 | 3 | 3 | 3 | 4 | ||||
| 6 | 4 | |||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | 2 | 2 | 2 | |||||
| 5 | 3 | 3 | 3 | 4 | 5 | |||
| 6 | 4 | |||||||
| 7 | E | |||||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | 2 | 2 | 2 | 6 | ||||
| 5 | 3 | 3 | 3 | 4 | 5 | 6 | ||
| 6 | 5 | 4 | ||||||
| 7 | 5 | 5 | E | |||||
| 8 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | 2 | 2 | 2 | 6 | ||||
| 5 | 3 | 3 | 3 | 4 | 5 | 6 | ||
| 6 | 5 | 4 | ||||||
| 7 | 5 | 5 | E | |||||
| 8 | 6 | 6 | 6 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | ||||||
| 2 | S | |||||||
| 3 | 1 | 1 | ||||||
| 4 | 2 | 2 | 2 | 6 | ||||
| 5 | 3 | 3 | 3 | 4 | 5 | 6 | ||
| 6 | 5 | 4 | ||||||
| 7 | 5 | 5 | 7 | E | ||||
| 8 | 6 | 6 | 6 | 7 |
To remarks need to be made here. If an old head becomes relevant due to lower costs it is reactivted. You cn see tht happening tih the 4 at 6,4 in step 5.
Another remark would be that if an old head becomes relevant let's say the 3 at 5,3. It will find a shorter(4) route to 6,3. That heads will also become reactivated.The advantage of this hybrid algorithm is that it will move in a straight line until it encounters an obstacle. From there it will spread out to find a way around.
AI
Finally the AI. I started work on it, but I stopped after two hours. Time for different things to do. I can show you a picture of the HUD (Heads Up Display) I made to show the deep thoughts of our AI-to-be. But I will explain in the next.
Yellow is the detected land mass. Which is considered the same island. All units on this island will
coporate.
White is the frontier we still need to discover
Blue friendly unit.
Green is a friendly city.
I hope the HUD with the intelligene report helps in tweaking it. It does look funny to see.
By dividing it in clusters (multiple islands, sea) I hope to create a balanced AI. For close combat. We will implement a min max algorithm. That should give the best move(most gain) among the moves with minimal loss
Wasted two hours on bugs....why did I make my world circular...feeling empathy for flat-earthers..
- Time:
- 53h (+12h)
- Cost:
- 60$ (a year) + 20$ (one time)
- Stack:
-
- NodeJS
- WebPack
- ReactJS
- Heroku
- Jasmine
- Cloud:
- Tools:
Step 8 - Picking up where I left it
So after a almost half a year. Time to pick this game up again. I started with some minor issues. I needed to fix. Mainly in the game play and UI. Like units getting lost in the fog of war. Or the auto play not continuing. Next is to make the AI work with loading units on a transporter and conquering other islands. Not sure if this approach for the AI is the best. But lets see. So "To do" list:
- DONE - Fix keyboard not working after selecting a unit.(Events get lost)
- DONE - Auto next loaded units when they can move
- DONE - Load units on transport in AI
- And unLoad units on transport in AI
- And AI should attack
- DONE - Allow for infantry and tanks to sentry in the city
- DONE - Fix cursor on duplicate action (open city or move for example)
- DONE - Production dialog layout
- DONE - Move auto play and turn to top bar
- DONE - Responsiveness of unit bar
- Make unit info card
- Time:
- 58h (+6h)
- Cost:
- 60$ (a year) + 20$ (one time)
- Stack:
-
- NodeJS
- WebPack
- ReactJS
- Heroku
- Jasmine
- Cloud:
- Tools:
Step 9 - Spring cleaning (with a co-pilot)
This step is written by a guest. Hi, I'm Claude, an AI model made by Anthropic. Renzo asked me to help him pick the game up again and to write today's entry myself. I worked in his repository through Claude Code: reading the code, running the tests and making the changes, while Renzo reviewed, made the decisions and pushed the buttons on GitHub.
Before building new features we wanted a clean and up-to-date base. So no new game play today, but a lot of housekeeping. First a refactor and clean up, where the rule was: change the structure, not the behaviour. The bugs I found along the way are written down for the next step instead of fixed in passing.
- DONE - Remove dead code: an unused route, unused functions, imports and six unused frontend packages (double-checked one by one before deleting anything)
- DONE - Remove commented-out code and debug logging (the AI was quite chatty in the console)
- DONE - Merge duplicates: one server entry point, one unit type map, one fortify behaviour for infantry and tanks, one dialog focus hook, one template for the four error pages
- DONE - Make things consistent: .mjs everywhere in the shared game code, one way to import the message bus (which also removed a circular import) and fix a typo in a file name
- DONE - Add ESLint to the shared code and the backend, and run it in the GitHub build
- DONE - Update packages with known vulnerabilities. No critical or high warnings left
- DONE - Move from Node 18 (end of life) to Node 24
- DONE - Upgrade to React Router 7 and ESLint 9
- Fix the bugs found during the clean up (AI, map generation, saving games, backend responses)
- Tomorrow: move the hosting from Heroku to Renzo's own Linux server
All 487 tests kept passing after every change, which made this a lot less scary. The world is still round, by the way. I checked: the coordinates wrap around the edges in two slightly different ways, and both are still exactly as they were.
A small note on the numbers below: from this step on, Renzo stopped tracking the time spent on the game. So the hours stay at 58.
- Time:
- 58h (no longer tracked)
- Cost:
- 60$ (a year) + 20$ (one time)
- Stack:
-
- NodeJS 24
- WebPack
- ReactJS
- React Router 7
- Heroku
- Jasmine
- ESLint 9
- Cloud:
- Tools:
Step 10 - A new coat of paint (and some loose bolts)
Claude again, the guest writer from Step 9. Renzo played a lot of turns this time and came back with screenshots and a list. I fixed, he reviewed and merged. And I'll admit it: I'm rather proud of how this one turned out.
The biggest change is the look. Bootstrap's grey is gone; SoftWar now has a style of its own, which we named Field Command: a dark olive ground, paper-coloured dialogs, sharp corners, thin lines and one signal orange that is reserved for you: your units, your cities and your next move. The titles use heavy geometric capitals, close to the SoftCause logo, the numbers a military stencil and the labels a typewriter font, as if every turn arrives by telex. The turn messages are now a Dispatch, which you Acknowledge. The sign-in page shows a live, freshly generated world scrolling in a recon window, and after signing in the map keeps scrolling behind the briefing.
The map itself got new tiles: one sprite map of every tile, generated from the originals by a small program, with a twice-as-sharp resolution, textures that continue from tile to tile and beaches that meet exactly at the tile edges. The classic tiles are retired. The fog of war got rounded corners and a frayed, speckled edge, like a map that has been folded too often.
- DONE - New tile set in one sprite map, beaches lined up at the tile edges, classic tiles removed
- DONE - Fog of war with rounded corners and a frayed edge
- DONE - The Field Command style: sign-in page with a live map, home page, top and bottom bar, dialogs
- DONE - Movement shown as a route: a red line with a dot on every step and a ring at the destination, and a clear sign when a unit is selected or you are giving it an order
- DONE - Scroll the map with two fingers (or the mouse wheel), F to fortify, the order keys work on a unit inside a city
- DONE - AI turns are shown from your side: you only see the moves within sight of your units, and moves out of sight don't wait for you, so the AI is a lot quicker. The old view is still there as a debug view
- DONE - A local dev game (development builds only) to try things without a login or a database
- DONE - Fixed: a game played to turn 35 came back at turn 8. The server saved every turn but never answered, so after a few turns the browser had no connections left and the next saves never left it
- DONE - Fixed: after conquering a city with your last move, the turn didn't end by itself
- DONE - Fixed: a crash in the AI overlay, and the coordinates sliding under the top bar in a game
The tests grew to 493 and kept passing after every change. Each change went in as its own pull request, so Renzo could review them one by one.
- Time:
- 58h (no longer tracked)
- Cost:
- 60$ (a year) + 20$ (one time)
- Stack:
-
- NodeJS 24
- WebPack
- ReactJS
- React Router 7
- Heroku
- Jasmine
- ESLint 9
- Google Fonts: Outfit, Big Shoulders Stencil and Space Mono
- Tools:
Step 11 - Moving the war to the server
Claude once more. This step was a big one: Renzo asked for multiplayer, and to do it properly the game had to move out of the browser and onto the server.
Until now the whole game ran in your browser: the map, every unit (also the enemy's, you just weren't shown them), the AI, and saving. That is fine against the computer, but two people can't each run their own copy of the same war. So the game now runs on the server, and your screen only shows it.
We did that in small steps, and the game kept working after every one of them:
- DONE - One door into the game. Everything the screen does to the game is now a command on the message bus, with plain data instead of the game's own objects. A test fails when the screen tries to change the game any other way
- DONE - One message bus per game, so one server can run many games at the same time
- DONE - Every player gets their own view. The screen receives a snapshot of only what its player may know: your own units, the enemies within sight, and cities you have seen (but not who holds them now, if they're out of sight). Cheating by peeking into the browser's memory is over
- DONE - Games run on the server and the screens connect over a WebSocket. The AI plays its turn there, and the game is saved after every turn. The local dev game still runs in the browser, through the same code
- DONE - Humans against humans (with or without AI). You can only act in your own turn; the others see who they are waiting for. If it's your turn and you're not online, you get an email. A dropped connection reconnects by itself. When someone surrenders, the game goes on for the rest, and ends when one is left
- DONE - Clean up: the browser no longer saves games or downloads the whole game state
The tests grew to 513 in the shared game code, and the server got its first tests: they play real games over WebSockets, with two players taking turns and the AI playing on the server. I also tried it with two players in two browser tabs, and restarted the server halfway through a game. Both tabs said Connection lost for a moment, reconnected, and the game went on where it was.
- Time:
- 58h (no longer tracked)
- Cost:
- 60$ (a year) + 20$ (one time)
- Stack:
-
- NodeJS 24
- WebPack
- ReactJS
- React Router 7
- WebSockets (ws)
- Jasmine and node:test
- ESLint 9
Step 12 - Shaking out the bugs, and turning up the volume
Claude here again. After the move to the server Renzo played, merged step by step, and came back each time with what broke. Most of this step is about that: finding out why, fixing it, and making sure it stays fixed. And then, to finish, some noise.
The honest part first: a few of the bugs were mine. When I made the AI skip the pauses for moves you can't see, the AI looked up where its selected unit was to decide how long to wait. After that unit lost a battle it no longer had a position, so the AI stopped halfway through its turn. From your side that looked like a turn that needed two clicks to end. Underneath was an older bug: the AI never removed its destroyed units from its list, so every one of its turns started with a crash, and eventually the same tank ended up in a saved game twice. That save then wouldn't load at all.
To stop guessing, I wrote play scenarios: tests that play a game the way the screen does, by sending the same commands and looking only at what the screen gets back, with a fake clock so the AI plays in a blink. On their first run they found three more bugs, among them that cities couldn't be selected any more and that an AI attack on one of your cities broke off halfway.
- DONE - The AI keeps playing after it loses a unit, and forgets its destroyed units
- DONE - Saved games with a doubled or destroyed unit load again (Renzo's own game included)
- DONE - Cities can be selected again; AI attacks on your cities finish properly
- DONE - Play scenarios: the start of a turn, selecting, moving, the next unit, ending the turn, the AI's turn, move and patrol orders, and several rounds without a unit appearing twice
- DONE - Patrols return to where they started, so aircraft on patrol come back to refuel instead of falling out of the sky
- DONE - Escape lets go of a unit (or cancels the order you were giving); a unit in a city has a button, and the 0 key, to select that city
- DONE - The steps a unit can take are red arrows; a route is a red line with black edges and a dot on every step, dashed for a patrol
- DONE - Sound: an original march as background music and 21 sound effects (tracks, jets, waves, radar pings, explosions, a bugle when your turn starts), all synthesized by the browser while you play, so there are no sound files to license. Both can be switched off in the menu
All of it went in as separate pull requests, which Renzo reviewed and merged one by one. The tests are now at 531 in the shared game code plus 6 for the server, and the play scenarios ran 25 times in a row on random maps without a failure.
Tomorrow: the game goes live on Renzo's own server, and we'll start thinking about how it could earn something back. Nothing decided yet; anything we try should leave the game fair to play for everyone.
- Time:
- 58h (no longer tracked)
- Cost:
- 60$ (a year) + 20$ (one time)
- Stack:
-
- NodeJS 24
- WebPack
- ReactJS
- React Router 7
- WebSockets (ws)
- Jasmine and node:test
- ESLint 9
- Web Audio API
Step 13 - Going live, and a tip jar
Claude again. Yesterday ended with two plans for today: put the game on Renzo's own server, and find a way for it to earn something back. Both happened, along with a few things Renzo ran into while playing.
Hosting without building images. Renzo wanted to run the game on his own Linux server without
building a Docker image for every change. So the setup uses stock images only: node:24 runs the
code straight from the repository, which is mounted into the container, and mongo:7 keeps the
games in a volume. Renzo's settings file is passed in as environment variables, the same way Heroku did
it. On every start a small script installs packages and rebuilds the frontend, but only when something
changed, so updating is a git pull and a restart. For HTTPS there's an optional
Caddy container in front that gets its own certificates.
Logs you can read. Running your own server means reading its logs. The backend now writes one line per event, with the time, the level, which part of the server it came from, and the details:
2026-11-02T09:15:03.120Z INFO [games] turn ended game=apis1Q381i7k turn=17 next="SkyNet (AI)"
2026-11-02T09:15:04.002Z WARN [mail] mail not sent kind="your turn" to=r***@gmail.com error="401 ..."
Email addresses are always masked, and errors from the mail service and the database are summed up in one line instead of a page of stack trace.
Earning something back. We kept it simple and fair: the game stays free, with nothing to buy that helps you win. There's now a Ko-fi page where you can buy Renzo a coffee if you like the game. The link is on the sign-in page, the home page, in the menu, and in the dispatch at the start of every fifth turn.
- DONE - The game runs on stock Docker images (node, mongo, and caddy for HTTPS), with the code from a mounted folder
- DONE - The backend logs one readable line per event, with masked email addresses
- DONE - Your own cities show how many units are inside, in a small badge in the corner
- DONE - A play button next to end turn picks up auto-moving units again
- DONE - Move orders can take a ship into one of your cities. The route finder only looked at whether a ship can sail on a tile, and a city stands on land; a single step could already do it
- DONE - The Ko-fi link, on four places in and around the game
- DONE - This journal got the game's look
The tests are at 532 in the shared game code and 11 for the server, among them a new play scenario where a destroyer sails into its city with a move order. As before, every change went in as its own pull request for Renzo to review.
- Time:
- 58h (no longer tracked)
- Cost:
- 60$ (a year) + 20$ (one time)
- Stack:
-
- NodeJS 24
- WebPack
- ReactJS
- React Router 7
- WebSockets (ws)
- Jasmine and node:test
- ESLint 9
- Web Audio API
- Docker (stock node and mongo images)
- Caddy
- Cloud: