So far, I have created a number of actors that together make up a simple, non-exciting simulation. However, controlling these actors is still done in an ad-hoc fashion.
In the next couple of posts, I will extend the simulation with supporting objects that make it easier to create and run different kinds of simulations. This post is dedicated to the theory behind these next steps.
In this post, I will introduce Actor states, the Controller, Rule sets and Settings.
Actor states and messages
Until now, I have created my actors and messages in a somewhat ad-hoc fashion. Basically, I required every actor to act on specific messages, and respond to them with a reply. Maybe it's time to let things make sense.
In fact, the current implementation already has some sense. Basically, there are five categories of messages:
- Processing requests – these messages ask an actor to do something with its resources on behalf of another actor. Examples are
Produce
andHarvest
; - Processing responses – these replies inform that an actor has done something with its resources. Examples are
Produced
andHarvested
; - Act requests – this message asks an actor to take action by itself. The only example so far is the
Act
message; - State responses – this message is used as a reply to
Act
requests, informing the caller about the current state of the actor; - Stop request – this message asks the actor to kill itself, and by its nature is the only message that has no reply;
Note the difference between the two kinds of requests: Processing requests are executed on behalf of another actor; Act requests allow for autonomous behavior. The fact that actors reply to Act requests by State responses might feel somewhat contrived. However, there is a good technical reason to use this pattern – it forces each Act
message to have a reply, making it possible to synchronize actors (by awaiting their replies) and write meaningful tests for them.
In the next few steps, I will expand on the messages, resulting in the following collection:
Type | Name | Applies to | Meaning |
---|---|---|---|
Processing requests | Produce | Producers | Please produce as much as possible from whatever you produce. |
Harvest | Harvesters | Please harvest as much as possible from your attached Producer. | |
Processing responses | Produced(name,amount) | Producers | I have produced amount materials of type name .
|
Harvested(name,amount) | Harvesters | I have harvested and stored amount materials of type name .
| |
Act messages | Act | every actor | Do whatever you need to do, and inform me of your current state. |
State responses | Working | every actor | I am fine and doing what I'm best at. |
Idle | Citizens, Harvesters | I am bored and have nothing to do. | |
Full | Harvesters | I am willing to work, but I can't store any goods. | |
Stop requests | Stop | every actor | Please kill yourself. |
The Controller
All my 'simulations' so far consisted of specification tests where I instantiated some actors, sent them some messages, and checked their responses. This is of course very useful for tests, but imagine using this approach for a simulation on the scale of a big Caesar IV scenario...
Enter the controller. The controller is an object that takes care of all the administrative work when setting up and running a simulation. Its responsibilities are:
- Managing the lifecycle of all actors;
- Keeping track of the state of all actors;
Managing the lifecycle ensures that we have a single point of entry for starting and stopping our simulation; the controller takes care of calling start
on all individual actors, and sending Stop
messages to all individual actors. This implies that we also have to register each actor with the controller.
Managing the lifecycle also includes sending out Act
messages to all individual actors; the replies are processed and make it possible for the controller to keep track of the state of all actors. The appropriate method is called tick
– meaning a single tick on the clock of the simulation.
Because we can synchronize on Act
messages by awaiting their state reply, tick
can ensure that each call advances the entire simulation by one clock tick. This technique is standard for a discrete event-driven simulations, and makes it easy to speed it up or slow it down.
Rule Sets
There are other problems with the current implementation, ones that are not simply solved by adding a controller. For starters, connecting Citizens to Harvesters and connecting Harvesters to Producers is done manually. Furthermore, adding Citizens is also done manually.
Why should this be a problem? Let's have a look at the reference games I've played: Caesar IV, The Settlers: Rise of an Empire and CivCity: Rome.
In Caesar, harvesters such as the Clay Mine can be placed anywhere on the map; they are then automatically connected to the closest Clay Pit. Settlers, on the other hand, places restrictions on the maximum distance from the harvester to the producer. Both games allow for an arbitrary number of harvesters to be connected to a single producer. CivCity in some cases requires the harvester to be placed on top of the producer, thereby automatically limiting the number of harvesters for that producer to one.
Also note that all three games have slightly different rules when it comes to harvesting food etc. For now, I will ignore this.
As for the citizens, all three games have radically different approaches. In Caesar, new citizens are added at a fixed rate as long as housing is available. They take the first job they can find which matches their place in society. In Settlers, adding a harvester automatically adds a citizen, which is immediately put to work at that harvester. In CivCity, new citizens arrive at a variable rate, based on the size of the city and number of available jobs. As soon as a job becomes available, they take it.
And that is just the tip of the iceberg. I hope I've made it clear that different games use different rules for the same kind of simulation. So, what rules to use in Scalasim?
The correct answer of course is: all of them. I will do this by introducing Rule sets. A Rule set is a Factory object that uses the Strategy pattern to create the right kind of controller. By playing around with the Rule set, you can change the entire game.
Settings
Another problem that should be tackled is the instantiation of my producers, harvesters and citizens. So far, my tests contained phrases like new Producer("Sand pit", "sand")
, thereby creating a new Sand pit that produces sand. However, the exact kind of producers, harvesters and citizens once again differ for each game. Below, I've listed some examples for each game.
Game | Producers | Harvesters |
---|---|---|
Caesar IV |
Gold mine Grain Field Olive Grove Sheep Pasture |
Gold Mining camp Grain Farm Olive Farm Sheep Farm |
The Settlers: Rise of an Empire |
Fish Quarry Beehive Grain Field |
Fishing Hut Stone Cutter Beekeeper's Hut Grain Farm |
CivCity: Rome |
Fish Trees Flax field Iron |
Fishing Jetty Wood Camp Flax farm Iron mine |
Apart from that, conversion speeds (such as the Harvester speed) and storage capacity also differ per game. These differences will be managed by creating a Setting. I'm not yet sure about the exact pattern, but it seems that the Prototype pattern will be a good fit.
Please note the difference between Settings and Rule sets. Settings define the kind of objects in our simulation; Rule sets define how these objects interact. Of course, separating these issues makes it possible to, for example, use the CivCity: Rome Setting with the Settlers Rule set.
What's next
All these concepts are nice in theory, but now it's time to actually implement them. The next few posts will be dedicated to just that.
After implementing these concepts, it will be possible to say stuff like:
val ruleset = TestRuleset val setting = TestSetting val controller = createController(ruleset, setting) setting producers foreach { p => controller.addProducer(p) } controller.start() controller.tick() setting harvesters foreach { h => controller.addHarvester(h) } setting harvesters foreach { h => controller.addHarvester(h) } controller.tick() controller.tick() controller.stop()
and get output like
--- [Coal mine #1] Stored 1 coal [Fishing hut #1] Stored 1 fish [Coal mine #2] Stored 1 coal [Fishing hut #2] Stored 1 fish --- [Coal mine #1] Stored 1 coal [Fishing hut #1] Stored 1 fish [Coal mine #2] Stored 1 coal [Fishing hut #2] Stored 1 fish --- [Coal mine #1] Is full [Fishing hut #1] Stored 1 fish [Coal mine #2] Is full [Fishing hut #2] Stored 1 fish ---
Let's do some programming.
No comments:
Post a Comment