#1002 Actor example

lbertrand Thu 4 Mar 2010

I am trying to use the actors concept to create a game of Rock/Paper/Scissors - nice example to understand the concept...

But it seems that my code will block at some point - don't know when - few games can be play or none...

Basically, I have 2 actors: 1 player which receive a message of type play and send back its choice to a coordinator which just create 2 players ask them to play and decide who is the winner...

Can someone have a look at this code and spot the obvious mistake...

And also, I would like to know what can be change in the code to make it more Fantom like... For example, on the receive method is there a nice way to not have to write a lot of if .. else .. if blocks?

And also, the coordinator neds to wait to receive responses from the 2 players, is there a better way to do that as opposed to my choice of storing 1st message in the locals?

#! /usr/bin/env fan

const mixin Move {}
const class Rock : Move {}
const class Paper : Move {}
const class Scissors : Move {}

const class Start {}
const class Play { const Coordinator coordinator;  new make(Coordinator coordinator) { this.coordinator = coordinator } }
const class Throw { const Move move; const Player player; new make(Player player, Move move) { this.player = player; this.move = move } }

const class Player : Actor
{
  const Str name
  const static Move[] moves := [Rock(), Paper(), Scissors()]  

  new make(Str name, ActorPool pool) : super(pool)
  {
    this.name = name
  }

  override Obj? receive(Obj? msg)
  {
    if (msg is Play)
    {
      play := msg as Play
      move := Player.moves[Int.random(0..Player.moves.size)]
      play.coordinator.send(Throw(this, move))
    }
    return null;
  }
}

const class Coordinator : Actor
{
  const Str LOCAL_THROW := "throw"
  const Player[] players

  new make(ActorPool pool) : super(pool)
  {
    Actor.locals[LOCAL_THROW] = null
    players = [Player("Player1", pool), Player("Player2", pool)]
  }

  override Obj? receive(Obj? msg)
  {
    if (msg is Start)
    { 
      players.each |Player p| { p.send(Play(this)) }
    }
    else if (msg is Throw)
    {
      aThrow := msg as Throw
      storedThrow := Actor.locals[LOCAL_THROW] as Throw
      if (null == storedThrow)
      {
        Actor.locals[LOCAL_THROW] = aThrow
      }
      else
      {
        Actor.locals[LOCAL_THROW] = null
        winner(storedThrow, aThrow)
        sendLater(1sec, Start())
      }
    }
    return null;
  }

  Void winner(Throw throwA, Throw throwB)
  {
    Str winner := "tie"

    switch ([throwA.move.typeof, throwB.move.typeof])
    {
      case [Rock#, Paper#] : winner = throwB.player.name
      case [Rock#, Scissors#] : winner = throwA.player.name
      case [Paper#, Rock#] : winner = throwA.player.name
      case [Paper#, Scissors#] : winner = throwB.player.name
      case [Scissors#, Rock#] : winner = throwB.player.name
      case [Scissors#, Paper#] : winner = throwA.player.name
    }

    echo("$throwA.player.name ($throwA.move.typeof.name), $throwB.player.name ($throwB.move.typeof.name) -> $winner")
  }
}

const class RockPaperScissors
{
  static Void main() {
    pool := ActorPool()
    Coordinator(pool).send(Start())
    Actor.sleep(10sec)
    pool.stop.join
  }
}

brian Thu 4 Mar 2010

@lbertrand

Your program doesn't work because of this line:

move := Player.moves[Int.random(0..Player.moves.size)]

Should be:

move := Player.moves[Int.random(0..<Player.moves.size)]

If you put a catch in there you'll see the exception being dumped. With that fix seems to run as expected.

A more "Fantom way" for writing the randomization of moves would be this:

enum class Move { rock, paper, scissors }

randMove := Move.vals.random

I'd also probably move winning logic to Move something like this:

enum class Move 
{
  ...
  Bool beats(Move m) { ... }
}

I rarely create message classes for my actors - only when I have a complicated message routing mechanism. Most of the time I just pass strings or lists.

lbertrand Thu 4 Mar 2010

Many thanks for your comments...

In Java I will have seen this problem in the random... I suppose the use of the nice Fantom feature of range just made me forgot about the last index!

Regarding your remark around messages... How will you link the actors together if I don't pass them around in the message?

brian Thu 4 Mar 2010

In my designs things are linked together with sys::Service. Under the covers a service may use one or multiple actors, but in general I never expose actor directly as part of a public API. It is an internal implementation detail.

lbertrand Thu 4 Mar 2010

Didn't know about this class at all - Thanks.

In my particular case, I am writing this Fantom script to show the Actor model and API of Fantom, as a light tutorial, so I want to expose it ;-)

In my case, the Start and Play messages can be pure string... But the throw one, I need to be able to know which player has chosen which move, so I need this information in the message... Any other way it may be done using a simple string message?

Also, I really don't like the way I have to store the 1st Throw message received and wait for the 2nd to arrive before I decide who wins?

Will it be possible to use the coalesce method so that the Throw messages are coalesce into a DoubelThrow message, and the coordinator only has to react on the DoubleThrow message, not on each Throw... But is it possible... Can we do something special in the receive method to leave message of type Throw in the queue?

brian Thu 4 Mar 2010

Any other way it may be done using a simple string message?

For quickies I tend to use a List as a tuple, in this case I probably would have passed [player, move] instead of creating a class.

Will it be possible to use the coalesce method so that the Throw messages

You could coalesce them, but wouldn't really solve your problem. Coalescing only works for messages backing up on the queue - it doesn't stall deliver until a condition is met.

lbertrand Thu 4 Mar 2010

I have reworked my example to take into account all comments...

I think it is now very Fantom-like, using the useful reflection to automatically call the right method when receiving a message...

Please, comment if more can be done... I don't want the code to become too cryptic either...

Brian, Andy, if you like this example, I do not mind you adding it to the examples directory (just nice to have my name somewhere ;-) Laurent Bertrand)

#! /usr/bin/env fan

enum class Move {
  rock, paper, scissors

  Bool beats(Move other) {
    switch ([this, other]) {
      case [paper, rock]     :
      case [rock, scissors]  :
      case [scissors, paper] : return true
      default: return false
    }
  }
}

const class Player : Actor {
  const Str name
  const Coordinator coordinator

  new make(Str name, Coordinator coordinator) : super(coordinator.pool) {
    this.name = name
    this.coordinator = coordinator
  }

  override Obj? receive(Obj? msg) {
    if (msg == "play") { this.coordinator.send(["move", name, Move.vals.random]) }
    return null
  }
}

const class Coordinator : Actor {
  const Player[] players

  new make(ActorPool pool) : super(pool) {
    players = [Player("player1", this), Player("player2", this)]
  }

  override Obj? receive(Obj? msg) { callOn(msg) }

  private Obj? callOn(Obj?[] msg) { Coordinator#.method(msg.first).callOn(this, msg[1..-1]) }

  private Void start() { players.each |Player p| { p.send("play") } }

  private Void move(Str name, Move move) {
    tuple := Actor.locals["coordinator"] as List
    if (null == tuple) {
      Actor.locals["coordinator"] = [name, move]
    } else {
      Actor.locals["coordinator"] = null
      winner(tuple[0], tuple[1], name, move)
      sendLater(1sec, ["start"])
    }
  }

  private Void winner(Str playerA, Move moveA, Str playerB, Move moveB) {
    Str winner := "tie"
    if (moveA.beats(moveB))  { winner = playerA }
    else if (moveB.beats(moveA)) { winner = playerB }

    echo("$playerA ($moveA), $playerB ($moveB) -> $winner")
  }
}

const class RockPaperScissors {
  static Void main() {
    pool := ActorPool()
    Coordinator(pool).send(["start"])
    Actor.sleep(10sec)
    pool.stop.join
  }
}

KevinKelley Thu 4 Mar 2010

Neat example! I was hoping to make that line:

Coordinator#.method(msg.first).callOn(this, msg[1..-1]) 

look a little neater with dynamic invoke, but I can't seem to get the -> operator to play nicely with variable-arguments. But this works:

this.trap(msg.first, msg[1..-1])

as a slightly less verbose variation.

I like that enum use pattern, too.

brian Fri 5 Mar 2010

@Laurent,

I don't have your email address - can you email directly at briansfrank on gmail, and we can talk about getting the example into distro.

Login or Signup to reply.