An angel of the Lord appeared to them, and the glory of the Lord shone around them, and they were terrified. Luke 2:9
It's that time of year again, where the F# community get together to source a collection of weird, wonderful and occasionally useful blog posts on life, the universe and sometimes Christmas.
As mentioned in last years post, I like to go back to the source when it comes to advent posts, so lets dive back into the book of Luke (and learn about agent based programming as we go).
The plan
We're going to simulate the angelic choir as they sing for the shepherds, although with a couple of minor limitations. One is I don't feel like dealing with cross platform audio issues (and don't think I could do the voices justice anyway…) and the other is that I can't draw for toffee.
So we're going to simulate a view of the angels from a long way away out of earshot.
The final result should end up looking something like this (your results may vary depending on console colour scheme, but I'd suggest dark background for the best effect!):
Step 1: atomic writes to the console
If you've tried to use the System.Console
namespace in .net, you'll have discovered a few
things about it. The biggest problem we want to deal with, is that writing a character in colour
to the console is not atomic.
You have to:
1 2 3 4 5 6 |
|
In async code, different threads doing this at the same time will mix these operations up, as there's no way to know what an other thread is doing with the cursor while you try and set up your own write.
For this we're going to set up our first agent: the console agent. It will be responsible for all writes to the screen in our program.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
The (|ConsoleColour|)
construct is what's called an active pattern. With it, we can pattern
match on any integer and be guaranteed to get a valid ConsoleColor enum out. It also spells
"colour" correctly :D.
Then we start a MailboxProcessor
(the default name for an agent in F#). This agent listens
for messages which consist of: an x coordinate, a y coordinate, an int for colour and a character
to write. The overall agent is implemented as an async block and so will not block a thread while
waiting for messages - but it will guarantee that it will not start processing the next message
until the current one is finished.
Hey presto! We can now safely write to the console from any thread simply by calling console.Post.
We'll try it out by creating some random stationary angels.
First, we'll initialize some infinite sequences of random numbers:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Then we'll wrap the write in an async method, and draw our angels across the screen concurrently; each angel will wait 50 milliseconds per unit across the x axis to give a nice staggered appearance.
You can find a full listing in advent1.fsx. Running it should give you something like this:
But the angel said to them, “Do not be afraid. I bring you good news that will cause great joy for all the people. Today in the town of David a Savior has been born to you; he is the Messiah, the Lord. This will be a sign to you: You will find a baby wrapped in cloths and lying in a manger.” Luke 2:10-12
Step 2: Add event loop
Onwards! Time to make our angels move. Following on with the theme, we're going to make an agent responsible for ticking off each 'loop' of events.
We'll add some safety to our console agent to make sure that writes outside the console don't cause us issues:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
Notice the use of the X and Y active patterns to enforce our domain constraints on the underlying .net type.
We'll also have some types for keeping track of an angels position and velocity.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Here we've defined + and - on a two element vector, and a helper function to calculate the vectors magnitude.
Now we're ready to set up our event loop agent. I'm going to call mine ping
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
This agent is a bit more chunky. If you look down to the end of the body, you'll see it starts
by calling init
. This method is responsible for waiting for the initial list of angels that
will populate our night sky. The angels themselves will be agents that listen for the AngelMessage
type.
init
sends an Init
message to each angel, asking it for it's initial position and velocity.
The message consists solely of a reply channel which the angel will use to pass back the information.
Once all the angels have reported in, we pass control to the recursive inner loop. On each round
through, the ping
agent asks every angel where it's moving to. It then writes spaces to every square on the console that held an angel last
tick, and finally draws the new positions of every angel.
And most of our infrastructure is in place! Let's test it with a collection of angels that will start with a random position and velocity and move in a straight line for a while.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Each of our angels knows how to report its initial state, and how to apply a function called logic
to it's previous state to generate the new position. For testing, the logic
we're passing in is just to add its velocity to it's current position each time its asked.
Full listing is in advent2.fsx, and running it should give us something like this:
Suddenly a great company of the heavenly host appeared with the angel, praising God and saying,
“Glory to God in the highest heaven, and on earth peace to those on whom his favor rests.” Luke 2:13-14
Adding some dancing
But! Angels in straight lines doesn't sound much fun. We'll make our angels a bit more interesting by implementing a simple boid variant.
First we'll add the ability to specify a colour as part of our angel info (check the full listing for details). We'll also expand the vectors to implement multiplication, division and a magnitude limit.
Then we can add a logic
module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
Nothing super exciting here individually - we have methods for discovering other angels nearby
(surrounding
), the average velocity of a group of angels (desiredVel
) and a rough guess
at not running into a group of nearby angels (avoid
). All could probably be improved!
Putting it all together, the boid
method calculates the acceleration the angel would "like" to
have to follow all if its rules fully, and then limits that by a specified maximum acceleration.
I played with the weighting of the rules a bit to get something that looked kind of nice, and also
decided to make my life easier by aiming cohesion towards the middle of the screen rather than the
middle of the flock.
Generating our angels is now just a case of partially applying boid with the parameters of our choice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
The ones in listing advent3.fsx give something reasonably nice, looking like:
One word of warning: there's a bug in the avoidance which I haven't had a chance to track down, so if you add too many angels they'll all push each other into the top left corner. Oops.
And that's all for now. I hope you enjoyed this brief dive into agent based programming, and how we can use agents to separate responsibility and protect against unwanted race conditions.
As you can see, this framework allows easy modification of angel logic, and in fact allows for every angel to have its own implementation without much added complexity - as long as it replies to the same messages.
Happy Christmas, and God bless.