Wednesday, September 8, 2010

Tying it together - Theory

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 and Harvest;
  • Processing responses – these replies inform that an actor has done something with its resources. Examples are Produced and Harvested;
  • 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:

TypeNameApplies toMeaning
Processing requestsProduceProducersPlease produce as much as possible from whatever you produce.
HarvestHarvestersPlease harvest as much as possible from your attached Producer.
Processing responsesProduced(name,amount)ProducersI have produced amount materials of type name.
Harvested(name,amount)HarvestersI have harvested and stored amount materials of type name.
Act messagesActevery actorDo whatever you need to do, and inform me of your current state.
State responsesWorkingevery actorI am fine and doing what I'm best at.
IdleCitizens, HarvestersI am bored and have nothing to do.
FullHarvestersI am willing to work, but I can't store any goods.
Stop requestsStopevery actorPlease 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.

GameProducersHarvesters
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