Files

148 lines
3.5 KiB
Go

package rtpconn
import (
"crypto/rand"
"fmt"
"math/big"
"regexp"
"strconv"
"strings"
"git.stormux.org/storm/skald/hall"
)
const (
maxDiceCount = 100
maxDiceSides = 1_000_000
maxDiceModifier = 1_000_000
dicePreviewSize = 5
)
var diceExpressionRegexp = regexp.MustCompile(
`^\s*([0-9]+)\s*[dD]\s*([0-9]+)(?:\s*([+-])\s*([0-9]+))?\s*$`,
)
type diceRoll struct {
Count int64
Sides int64
Modifier int64
Results []int64
Total int64
}
func parseDiceExpression(expression string) (diceRoll, error) {
match := diceExpressionRegexp.FindStringSubmatch(expression)
if match == nil {
return diceRoll{}, hall.UserError(
"invalid dice roll; example: /2d6 + 3",
)
}
count, err := strconv.ParseInt(match[1], 10, 64)
if err != nil || count < 1 || count > maxDiceCount {
return diceRoll{}, hall.UserError(
fmt.Sprintf("dice count must be between 1 and %d", maxDiceCount),
)
}
sides, err := strconv.ParseInt(match[2], 10, 64)
if err != nil || sides < 2 || sides > maxDiceSides {
return diceRoll{}, hall.UserError(
fmt.Sprintf("die sides must be between 2 and %d", maxDiceSides),
)
}
var modifier int64
if match[4] != "" {
modifier, err = strconv.ParseInt(match[4], 10, 64)
if err != nil || modifier > maxDiceModifier {
return diceRoll{}, hall.UserError(
fmt.Sprintf("dice modifier must be between -%d and %d", maxDiceModifier, maxDiceModifier),
)
}
if match[3] == "-" {
modifier = -modifier
}
}
return diceRoll{Count: count, Sides: sides, Modifier: modifier}, nil
}
func generateDiceRoll(roll diceRoll) (diceRoll, error) {
maximum := big.NewInt(roll.Sides)
roll.Results = make([]int64, roll.Count)
roll.Total = roll.Modifier
for i := range roll.Results {
result, err := rand.Int(rand.Reader, maximum)
if err != nil {
return diceRoll{}, err
}
roll.Results[i] = result.Int64() + 1
roll.Total += roll.Results[i]
}
return roll, nil
}
func formatDiceResults(results []int64) string {
shown := len(results)
if shown > dicePreviewSize {
shown = dicePreviewSize
}
parts := make([]string, shown)
for i := range parts {
parts[i] = strconv.FormatInt(results[i], 10)
}
if len(results) > dicePreviewSize {
return strings.Join(parts, ", ") + fmt.Sprintf(", and %d more", len(results)-shown)
}
if shown == 1 {
return parts[0]
}
if shown == 2 {
return parts[0] + " and " + parts[1]
}
return strings.Join(parts[:shown-1], ", ") + ", and " + parts[shown-1]
}
func formatDiceAnnouncement(username string, roll diceRoll) string {
dieWord := "dice"
if roll.Count == 1 {
dieWord = "die"
}
modifier := ""
if roll.Modifier > 0 {
modifier = fmt.Sprintf(", plus %d", roll.Modifier)
} else if roll.Modifier < 0 {
modifier = fmt.Sprintf(", minus %d", -roll.Modifier)
}
return fmt.Sprintf(
"%s rolls %d %d-sided %s: %s%s, for a total of %d.",
username, roll.Count, roll.Sides, dieWord,
formatDiceResults(roll.Results), modifier, roll.Total,
)
}
func handleDiceRoll(c *webClient, g *hall.Hall, value any) error {
if !member("message", c.Permissions()) {
return c.error(hall.UserError("not authorised"))
}
expression, ok := value.(string)
if !ok {
return c.error(hall.UserError("invalid dice roll; example: /2d6 + 3"))
}
roll, err := parseDiceExpression(expression)
if err != nil {
return c.error(err)
}
roll, err = generateDiceRoll(roll)
if err != nil {
return c.error(err)
}
announcement := formatDiceAnnouncement(c.Username(), roll)
if err := broadcastServerChat(g, "Dice", "dice", announcement); err != nil {
return c.error(err)
}
return nil
}