Dienstag, 8. Dezember 2015

Akka.NET Adventskalender – Tür 8

Ich weiß was, das Du nicht weißt

Willkommen zurück! Wie gestern versprochen werden wir heute zusammen ein kleines Spiel programmieren. Sicher hast Du als Kind mit Deinen Spielkameraden auch "Zahlen Raten" gespielt. Einer sucht sich eine Zahl aus und der Andere versucht sie durch geschicktes Fragen einzukreisen bis die Zahl erraten ist. Hierzu wird eine Vermutung geäußert und als Antwort erfährt man ob die Vermutung zu klein, zu groß oder richtig war. Heute sind wir erfahrene Entwickler und wissen, was binäre Suche ist. Insofern haben wir den Lösungsweg schon im Kopf.

Wie werden die Aktoren miteinander reden?

Der einfachste Weg, dieses Spiel zu programmieren, ist für die jeweiligen Verantwortlichkeiten einen Aktor einzusetzen.

Der Erste – nennen wir ihn Chooser sucht sich eine zufällige Zahl im Bereich von 1 bis 100 aus. Die einzige Nachricht, auf die er antworten muss, ist die Bitte, einen Versuch zu prüfen. Nennen wir die Klasse der Nachricht einfach TestTry. Auf diese Anfrage erhalten wir eine von drei Antworten: TryTooBig, TryTooSmall oder Guessed.

Der zweite Mitspieler, nennen wir ihn Enquirer, kennt den Chooser und bittet ihn wiederholt, eine Zahl zu prüfen. Aufgrund der Antwort wird der verbleibende Zahlenbereich eingeschränkt und erneut gefragt. Das ganze wiederholt sich solange bis Guessed als Antwort eintrifft. Damit ist das Spiel vorüber.

OK, fangen wir an. Heute werden wir eine Datei pro Nachrichten-Klasse anlegen und alle Nachrichten in ein Verzeichnis (und damit einen eigenen Namensraum) legen. Selbstverständlich steht es Dir frei, es anders zu machen.

Die Nachrichten Klassen könnten so wie folgt ausehen. An dieser Stelle habe ich trotz der Wiederholungen aufgrund der einfacheren Lesbarkeit auf eine gemeinsame Basisklasse verzichtet. Auch hier darf Deine Implementierung gerne anders ausfallen.

namespace GuessMyNumber.Messages
{
    public class TestTry
    {
        public int Number { get; private set; }

        public TestTry(int number)
        {
            Number = number;
        }
    }
}

namespace GuessMyNumber.Messages
{
    public class TryTooBig
    {
        public int Number { get; private set; }

        public TryTooBig(int number)
        {
            Number = number;
        }
    }
}

namespace GuessMyNumber.Messages
{
    public class TryTooSmall
    {
        public int Number { get; private set; }

        public TryTooSmall(int number)
        {
            Number = number;
        }
    }
}

namespace GuessMyNumber.Messages
{
    public class Guessed
    {
        public int Number { get; private set; }

        public Guessed(int number)
        {
            Number = number;
        }
    }
}

Das war nicht schwer. Einfache Klassen mit jeweils nur einer Eigenschaft. Gerne dürfen die natürlich auch in einer Datei definiert werden – ganz nach Deinem Geschmack oder Gewohnheit.

Der Chooser könnte so wie nachfolgend aussehen. Wir werden ihn morgen nochmals anpassen um ihn flexibler zu machen, aber für heute sollte das ein brauchbarer Anfang sein:

using System;
using Akka.Actor;
using GuessMyNumber.Messages;

namespace GuessMyNumber.Actors
{
    public class Chooser : ReceiveActor
    {
        private int mySecretNumber;

        public Chooser()
        {
            var generator = new Random();
            mySecretNumber = generator.Next(1, 101);

            Console.WriteLine("Pssst: my secret is: {0}", mySecretNumber);

            Receive<TestTry>(t => HandleTestTry(t));
        }

        private void HandleTestTry(TestTry testTry)
        {
            var triedNumber = testTry.Number;

            Console.WriteLine("Received Guess: {0}", triedNumber);

            if (triedNumber < mySecretNumber)
            {
                Sender.Tell(new TryTooSmall(triedNumber));
            }
            else if (triedNumber > mySecretNumber)
            {
                Sender.Tell(new TryTooBig(triedNumber));
            }
            else
            {
                Sender.Tell(new Guessed(triedNumber));
            }
        }
    }
}

Und schließlich folgt der Enquirer, der versucht die Zahl zu erraten. Er muss wissen, wen er fragt, daher statten wir den Konstruktur mit einem IActorRef Argument aus. Einzige Einschränkung dabei ist, dass wir so keine zirkulären Referenzen realisieren können und die Reihenfolge des Aufbaus der Aktoren entscheidend ist. In unserem Fall ist das problemlos. Diese Klasse benötigt deutlich mehr Logik, aber mit dem Hintergrundwissen der binären Suche ist die Logik sicher einfach nachvollziehbar. Selbstverständlich hätten wir uns den letzten Versuch auch selbst merken können, anstelle sowohl bei der Frage als auch der Antwort jeweils die Zahl mitzuführen. Wie immer führen mehrere Wege zum Ziel.

using System;
using Akka.Actor;
using GuessMyNumber.Messages;

namespace GuessMyNumber.Actors
{
    public class Enquirer : ReceiveActor
    {
        private readonly IActorRef chooser;

        // possible range of numbers including boundaries
        private int rangeFrom;
        private int rangeTo;

        public Enquirer(IActorRef chooser)
        {
            this.chooser = chooser;

            rangeFrom = 1;
            rangeTo = 100;

            Receive<TryTooSmall>(t =>HandleTooSmallTry(t));
            Receive<TryTooBig>(t => HandleTooBigTry(t));
            Receive<Guessed>(g =>HandleGuessed(g));

            MakeATry();
        }

        private void MakeATry()
        {
            var triedNumber = rangeFrom + (rangeTo - rangeFrom) / 2;

            Console.WriteLine("Range: {0} - {1}, trying: {2}",
                rangeFrom, rangeTo, triedNumber);

            chooser.Tell(new TestTry(triedNumber));
        }

        private void HandleTooSmallTry(TryTooSmall guessTooSmall)
        {
            rangeFrom = guessTooSmall.Number + 1;
            MakeATry();
        }

        private void HandleTooBigTry(TryTooBig guessTooBig)
        {
            rangeTo = guessTooBig.Number - 1;
            MakeATry();
        }

        private void HandleGuessed(Guessed guessed)
        {
            Console.WriteLine("Guessed: {0}", guessed.Number);

            Context.System.Shutdown();
        }
    }
}

Und wie immer brauchen wir eine Anwendung, innerhalb derer wir das ActorSystem starten und das Ganze ans Laufen bringen:

using System;
using Akka.Actor;
using GuessMyNumber.Actors;

namespace GuessMyNumber
{
    class MainClass
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Number Guess Starting");

            var system = ActorSystem.Create("Numb3rs");
            var chooser = system.ActorOf(
                Props.Create<Chooser>(),
                "Chooser"
            );
            var enquirer = system.ActorOf(
                Props.Create<Enquirer>(chooser),
                "Enquirer"
            );

            system.AwaitTermination();
        }
    }
}

Wie? Gar kein Shutdown() Aufruf? Das machen wir diesmal im Enquirer, wenn wir die Guessed Nachricht erhalten. Du errätst es schon, das ist sicher kein guter Stil, denn solche Aktoren sind bestimmt nicht wiederverwendbar. Also: nicht nachmachen außer heute...

Der Programmablauf könnte so aussehen:

Number Guess Starting
Pssst: my secret is: 70
Range: 1 - 100, trying: 50
Received Guess: 50
Range: 51 - 100, trying: 75
Received Guess: 75
Range: 51 - 74, trying: 62
Received Guess: 62
Range: 63 - 74, trying: 68
Received Guess: 68
Range: 69 - 74, trying: 71
Received Guess: 71
Range: 69 - 70, trying: 69
Received Guess: 69
Range: 70 - 70, trying: 70
Received Guess: 70
Guessed: 70

Damit haben wir heute gesehen, wie einfach sich Aktoren miteinander unterhalten können. Das einzig unschöne war, dass wir wissen mussten, wie wir die diversen Aktoren miteinander "verdrahten" müssen, damit das Spiel wie geplant läuft.

Das werden wir morgen verbessern.

Keine Kommentare:

Kommentar veröffentlichen