148 lines
3.5 KiB
Go
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
|
|
}
|