Lasst uns gemeinsam einen Webservice bauen, der Würfelwürfe wie zum Beispiel 3d6
(also drei sechseitige Würfel werden und die Ergebnisse aufaddieren) ausführen kann. Ich möchte das ganze in Go schreiben und dann auf Heroku hosten. Warum Go? Weil es mal etwas anderes ist und ich gerade Lust auf diese Programmiersprache habe. Warum Heroku? Weil sie Go unterstützen und eine einfache Installation nichts kostet.
Ich möchte eine Würfelformel (z.B. 3d6
) einfach in eine URL einbetten können und dann einen GET-Request damit machen. Das Ergebnis möchte ich in Form eines JSON-Dokuments bekommen:
$ curl http://dice.herokuapp.com/roll/3d6
{
"sum": 9,
"rolls: [3, 5, 1]
}
Für die Würfelbeschreibung beginne ich mit einem sehr einfachen Format, das aus einer Zahl, einem kleinen d
und einer weiteren Zahl besteht. Eine oder beide Zahlen können fehlen. Fehlt die erste, nehme ich 1
an. Fehlt die zweite, nehme ich 6
an.
Beginnen wir mit einer Funktion, die Ausdrücke der oben definierten Formelsystem versteht und auswerten kann.
// DiceResult represents a dice roll result.
type DiceResult struct {
Sum int
Rolls []int
}
// RollDice rolls a number of dice described by `dice`
// and returns the result.
func RollDice(dice string) *DiceResult {
re := regexp.MustCompile("(\\d*)d(\\d*)")
matches := re.FindStringSubmatch(dice)
if matches == nil {
return nil
}
var count, sides int
if len(matches[1]) > 0 {
n, err := strconv.ParseInt(matches[1], 10, 0)
if err != nil || n == 0 || n > 1000 {
return nil
}
count = int(n)
} else {
count = 1
}
if len(matches[2]) > 0 {
n, err := strconv.ParseInt(matches[2], 10, 0)
if err != nil || n == 0 {
return nil
}
sides = int(n)
} else {
sides = 6
}
var sum int
var rolls []int
for ; count > 0; count-- {
roll := rand.Intn(sides) + 1
rolls = append(rolls, roll)
sum += roll
}
return &DiceResult{sum, rolls}
}
Yeah, Go ist da ein bisschen unhandlich, was das Umwandeln der mittels regulärem Ausdruck ermittelten Zahlen in int
-Werte angeht. Ich bin einmal vorsichtig, und lasse maximal 1000 Würfelwürfe zu. Wenn mehr gewünscht werden oder sonst irgendetwas nicht passt, liefert RollDice
den Wert nil
zurück, um einen Fehler zu signalisieren.
Um das ganze auszuprobieren, füge ich noch schnell eine main
-Funktion hinzu und mache das ganze so zu einem Kommandozeilenprogramm:
func main() {
for i := 1; i < len(os.Args); i++ {
fmt.Printf("%v\n", RollDice(os.Args[i]))
}
}
Dabei fällt mir auf, dass Go offenbar nicht automatisch seinen Zufallszahlengenerator mit einem quasi-zufälligen Startwert initialisiert, sodass ich noch diese Zeile ergänzen muss:
rand.Seed(time.Now().UnixNano())
Nun kann ich Würfeln, indem ich
$ dice 3d6 1d20 100d100
ausführe.
Eigentlich wollte ich ja aber meine Antwort mit JSON ausdrücken. Dazu kann ich DiceResult
annotieren und dann die JSON-Bibliothek von Go benutzen:
// DiceResult represents a dice roll result.
type DiceResult struct {
Sum int `json:"sum"`
Rolls []int `json:"rolls"`
}
...
func main() {
rand.Seed(time.Now().UnixNano())
for i := 1; i < len(os.Args); i++ {
m, _ := json.Marshal(RollDice(os.Args[i]))
fmt.Printf("%s\n", m)
}
}
So sieht ein minimale HTTP-Server in Go aus:
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "I will roll %s", r.URL.Path[6:])
}
func main() {
http.HandleFunc("/roll/", handler)
http.ListenAndServe(":8080", nil)
}
Das ist angenehm einfach und deutlich netter als das Gefummel in RollDice
, was in einer Sprache wie JavaScript kürzer gewesen wäre.
Was bleibt, ist ein Aufruf von RollDice
in handler
:
func handler(w http.ResponseWriter, r *http.Request) {
dice := r.URL.Path[6:]
result := RollDice(dice)
if result == nil {
http.Error(w, "syntaxerror", http.StatusBadRequest)
} else {
m, _ := json.Marshal(result)
w.Header().Set("content-type", "application/json")
fmt.Fprintf(w, "%s", m)
}
}
func main() {
rand.Seed(time.Now().UnixNano())
http.HandleFunc("/roll/", handler)
http.ListenAndServe(":8080", nil)
}
Um den Webservice auf Heroku zu deployen, muss ich zunächst einmal den fest verdrahteten Port durch eine Umgebungsvariable ersetzen:
http.ListenAndServe(":"+os.Getenv("PORT"), nil)
Go benötigt ein zumindest für mich ungewohntes Setup und damit ich nicht alle meine Go-Programme gleichzeitig mittels git zu Heroku schiebe, muss ich mein dicews
-Projekt wie folgt anlegen:
$ cd .../develop
$ mkdir -p dicews/src/dicews
$ cd dicews
$ cp ... src/dicews/main.go
$ printf "dicews\n" > .godir
$ printf "bin\npkg\n" > .gitignore
$ printf "web: dicews\n" > Procfile
$ export GOPATH=`pwd`
$ go get dicews
$ git init
$ git add .
$ git commit -m "initial version"
...
Ich sollte meinen Webservice nach wie vor starten können:
$ PORT=8080 bin/dicews
Nun kann ich ein Projekt bei Heroku anlegen, nachdem ich mich dort angemeldet habe. Dazu muss ich den Go-Buildstack angeben, damit mein Dyno passend konfiguriert wird. Dann kann ich meinen Quelltext pushen und schließlich einen Dyno aktivieren, er ist Teil des kostenlosen Angebots.
$ heroku login
...
$ heroku create dicews -b https://github.com/kr/heroku-buildpack-go.git
$ git push heroku master
...
$ heroku ps:scale web=1
Nun kann ich z.B. https://dicews.herokuapp.com/roll/10d10 aufrufen und zufrieden über die fertige und deployte App sein.
Die Funktion zum Würfeln benötigt mehr Code, als gedacht, war aber einfach zu implementieren. Mein primitiver HTTP-Handler war noch einfacher. Am längsten habe ich tatsächlich für Heroku gebraucht, weil es wahrscheinlich schon mehr als ein Jahr her ist, als ich das das letze benutzt habe.
Jetzt müsste es noch eine Erklärungsseite /index.html
geben und ein /favicon.ico
, das der Browser auch gerne immer laden möchte.
Dann könnte man die Syntax für die Würfel aufbohren. Man könnte Terme und Konstanten addieren oder subtrahieren. Man könnte explodierende Würfe vorsehen. Man könnte noch d%
als Abkürzung für d100
erlauben und dF
als Abkürzung für d3-2
. Nett wäre auch, das Maximum oder Minimum von zwei Ausdrücken zu berechnen, was man für Savage World und seine Wild-Dice benutzen könnte oder für Advance und Disadvantage bei D&D. Einige Systeme brauchen die N besten oder schlechtesten Würfel von M Würfen. Und dann sind da noch die ganzen Dice-Pool-Systeme, wo Erfolge gezählt werden und nicht einfach nur Summen addiert werden.