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 }