Skip to content

Instantly share code, notes, and snippets.

@sma
Created May 14, 2015 20:41
Show Gist options
  • Save sma/8a701b73bea32b8d458e to your computer and use it in GitHub Desktop.
Save sma/8a701b73bea32b8d458e to your computer and use it in GitHub Desktop.
Ein simpler Webservice zum Werfen von n-seitigen Würfeln
package main
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"os"
"regexp"
"strconv"
"time"
)
// DiceResult represents a dice roll result.
type DiceResult struct {
Sum int `json:"sum"`
Rolls []int `json:"rolls"`
}
// 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}
}
func handler(w http.ResponseWriter, r *http.Request) {
dice := r.URL.Path[6:]
result := RollDice(dice)
if result == nil {
http.Error(w, "syntax error", 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(":"+os.Getenv("PORT"), nil)
}

Würfelwurfwebservice

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.

API

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]
}

Formelsyntax

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.

Implementierung

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)
	}
}

HTTP-Server

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)
}

Heroku

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.

Fazit

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment