Skip to content

Commit

Permalink
add seed and random number generator into Settings
Browse files Browse the repository at this point in the history
  • Loading branch information
robbles committed Apr 28, 2022
1 parent 1d89cc9 commit 5fc70a7
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 23 deletions.
178 changes: 174 additions & 4 deletions board.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package rules

import (
"math/rand"
)
import "math/rand"

type BoardState struct {
Turn int32
Expand Down Expand Up @@ -37,9 +35,10 @@ func (prevState *BoardState) Clone() *BoardState {
}
for i := 0; i < len(prevState.Snakes); i++ {
nextState.Snakes[i].ID = prevState.Snakes[i].ID
nextState.Snakes[i].Health = prevState.Snakes[i].Health
nextState.Snakes[i].Body = append([]Point{}, prevState.Snakes[i].Body...)
nextState.Snakes[i].Health = prevState.Snakes[i].Health
nextState.Snakes[i].EliminatedCause = prevState.Snakes[i].EliminatedCause
nextState.Snakes[i].EliminatedOnTurn = prevState.Snakes[i].EliminatedOnTurn
nextState.Snakes[i].EliminatedBy = prevState.Snakes[i].EliminatedBy
}
return nextState
Expand Down Expand Up @@ -336,3 +335,174 @@ func isKnownBoardSize(b *BoardState) bool {
}
return false
}

func placeSnakesAutomaticallyWithRand(rand Rand, b *BoardState, snakeIDs []string) error {
if isKnownBoardSize(b) {
return placeSnakesFixedWithRand(rand, b, snakeIDs)
}
return placeSnakesRandomlyWithRand(rand, b, snakeIDs)
}

func placeSnakesFixedWithRand(rand Rand, b *BoardState, snakeIDs []string) error {
b.Snakes = make([]Snake, len(snakeIDs))

for i := 0; i < len(snakeIDs); i++ {
b.Snakes[i] = Snake{
ID: snakeIDs[i],
Health: SnakeMaxHealth,
}
}

// Create start 8 points
mn, md, mx := int32(1), (b.Width-1)/2, b.Width-2
startPoints := []Point{
{mn, mn},
{mn, md},
{mn, mx},
{md, mn},
{md, mx},
{mx, mn},
{mx, md},
{mx, mx},
}

// Sanity check
if len(b.Snakes) > len(startPoints) {
return ErrorTooManySnakes
}

// Randomly order them
rand.Shuffle(len(startPoints), func(i int, j int) {
startPoints[i], startPoints[j] = startPoints[j], startPoints[i]
})

// Assign to snakes in order given
for i := 0; i < len(b.Snakes); i++ {
for j := 0; j < SnakeStartSize; j++ {
b.Snakes[i].Body = append(b.Snakes[i].Body, startPoints[i])
}

}
return nil
}

func placeSnakesRandomlyWithRand(rand Rand, b *BoardState, snakeIDs []string) error {
b.Snakes = make([]Snake, len(snakeIDs))

for i := 0; i < len(snakeIDs); i++ {
b.Snakes[i] = Snake{
ID: snakeIDs[i],
Health: SnakeMaxHealth,
}
}

for i := 0; i < len(b.Snakes); i++ {
unoccupiedPoints := getEvenUnoccupiedPoints(b)
if len(unoccupiedPoints) <= 0 {
return ErrorNoRoomForSnake
}
p := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
for j := 0; j < SnakeStartSize; j++ {
b.Snakes[i].Body = append(b.Snakes[i].Body, p)
}
}
return nil
}

// PlaceFoodAutomatically initializes the array of food based on the size of the board and the number of snakes.
func placeFoodAutomaticallyWithRand(rand Rand, b *BoardState) error {
if isKnownBoardSize(b) {
return placeFoodFixedWithRand(rand, b)
}
return placeFoodRandomlyWithRand(rand, b, int32(len(b.Snakes)))
}

func placeFoodFixedWithRand(rand Rand, b *BoardState) error {
centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2}

// Place 1 food within exactly 2 moves of each snake, but never towards the center or in a corner
for i := 0; i < len(b.Snakes); i++ {
snakeHead := b.Snakes[i].Body[0]
possibleFoodLocations := []Point{
{snakeHead.X - 1, snakeHead.Y - 1},
{snakeHead.X - 1, snakeHead.Y + 1},
{snakeHead.X + 1, snakeHead.Y - 1},
{snakeHead.X + 1, snakeHead.Y + 1},
}

// Remove any invalid/unwanted positions
availableFoodLocations := []Point{}
for _, p := range possibleFoodLocations {

// Ignore points already occupied by food
isOccupiedAlready := false
for _, food := range b.Food {
if food.X == p.X && food.Y == p.Y {
isOccupiedAlready = true
break
}
}
if isOccupiedAlready {
continue
}

// Food must be further than snake from center on at least one axis
isAwayFromCenter := false
if p.X < snakeHead.X && snakeHead.X < centerCoord.X {
isAwayFromCenter = true
} else if centerCoord.X < snakeHead.X && snakeHead.X < p.X {
isAwayFromCenter = true
} else if p.Y < snakeHead.Y && snakeHead.Y < centerCoord.Y {
isAwayFromCenter = true
} else if centerCoord.Y < snakeHead.Y && snakeHead.Y < p.Y {
isAwayFromCenter = true
}
if !isAwayFromCenter {
continue
}

// Don't spawn food in corners
if (p.X == 0 || p.X == (b.Width-1)) && (p.Y == 0 || p.Y == (b.Height-1)) {
continue
}

availableFoodLocations = append(availableFoodLocations, p)
}

if len(availableFoodLocations) <= 0 {
return ErrorNoRoomForFood
}

// Select randomly from available locations
placedFood := availableFoodLocations[rand.Intn(len(availableFoodLocations))]
b.Food = append(b.Food, placedFood)
}

// Finally, always place 1 food in center of board for dramatic purposes
isCenterOccupied := true
unoccupiedPoints := getUnoccupiedPoints(b, true)
for _, point := range unoccupiedPoints {
if point == centerCoord {
isCenterOccupied = false
break
}
}
if isCenterOccupied {
return ErrorNoRoomForFood
}
b.Food = append(b.Food, centerCoord)

return nil
}

// PlaceFoodRandomly adds up to n new food to the board in random unoccupied squares
func placeFoodRandomlyWithRand(rand Rand, b *BoardState, n int32) error {
for i := int32(0); i < n; i++ {
unoccupiedPoints := getUnoccupiedPoints(b, false)
if len(unoccupiedPoints) > 0 {
newFood := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
b.Food = append(b.Food, newFood)
}
}
return nil
}
6 changes: 6 additions & 0 deletions rand.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package rules

type Rand interface {
Intn(n int) int
Shuffle(n int, swap func(i, j int))
}
5 changes: 2 additions & 3 deletions royale.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package rules

import (
"errors"
"math/rand"
)

var royaleRulesetStages = []string{
Expand All @@ -19,7 +18,7 @@ var royaleRulesetStages = []string{
type RoyaleRuleset struct {
StandardRuleset

Seed int64
Seed int64 // Deprecated, StandardRuleset.Seed is used instead

ShrinkEveryNTurns int32
}
Expand Down Expand Up @@ -55,7 +54,7 @@ func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove)
return false, nil
}

randGenerator := rand.New(rand.NewSource(settings.RoyaleSettings.seed))
randGenerator := settings.Rand()

numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns
minX, maxX := int32(0), b.Width-1
Expand Down
9 changes: 5 additions & 4 deletions royale_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package rules

import (
"errors"
"math/rand"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -96,6 +95,7 @@ func TestRoyaleHazards(t *testing.T) {
}
r := RoyaleRuleset{
StandardRuleset: StandardRuleset{
seed: seed,
HazardDamagePerTurn: 1,
},
Seed: seed,
Expand Down Expand Up @@ -125,7 +125,7 @@ func TestRoyalDamageNextTurn(t *testing.T) {
seed := int64(45897034512311)

base := &BoardState{Width: 10, Height: 10, Snakes: []Snake{{ID: "one", Health: 100, Body: []Point{{9, 1}, {9, 1}, {9, 1}}}}}
r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 30}, Seed: seed, ShrinkEveryNTurns: 10}
r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 30, seed: seed}, Seed: seed, ShrinkEveryNTurns: 10}
m := []SnakeMove{{ID: "one", Move: "down"}}

stateAfterTurn := func(prevState *BoardState, turn int32) *BoardState {
Expand Down Expand Up @@ -261,15 +261,16 @@ func TestRoyaleCreateNextBoardState(t *testing.T) {
r := RoyaleRuleset{
StandardRuleset: StandardRuleset{
HazardDamagePerTurn: 1,
seed: 0,
},
ShrinkEveryNTurns: 1,
Seed: 0,
}
rand.Seed(0)
rb := NewRulesetBuilder().WithParams(map[string]string{
ParamGameType: GameTypeRoyale,
ParamHazardDamagePerTurn: "1",
ParamShrinkEveryNTurns: "1",
})
}).WithSeed(0)
for _, gc := range cases {
gc.requireValidNextState(t, &r)
// also test a RulesBuilder constructed instance
Expand Down
28 changes: 28 additions & 0 deletions ruleset.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rules

import (
"math/rand"
"strconv"
)

Expand Down Expand Up @@ -109,6 +110,7 @@ func (rb *rulesetBuilder) AddSnakeToSquad(snakeID, squadName string) *rulesetBui
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
func (rb rulesetBuilder) Ruleset() PipelineRuleset {
standardRuleset := &StandardRuleset{
seed: rb.seed,
FoodSpawnChance: paramsInt32(rb.params, ParamFoodSpawnChance, 0),
MinimumFood: paramsInt32(rb.params, ParamMinimumFood, 0),
HazardDamagePerTurn: paramsInt32(rb.params, ParamHazardDamagePerTurn, 0),
Expand Down Expand Up @@ -169,6 +171,7 @@ func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRulese
name: name,
pipeline: p,
settings: Settings{
seed: rb.seed,
FoodSpawnChance: paramsInt32(rb.params, ParamFoodSpawnChance, 0),
MinimumFood: paramsInt32(rb.params, ParamMinimumFood, 0),
HazardDamagePerTurn: paramsInt32(rb.params, ParamHazardDamagePerTurn, 0),
Expand Down Expand Up @@ -250,6 +253,9 @@ type Settings struct {
HazardMapAuthor string `json:"hazardMapAuthor"`
RoyaleSettings RoyaleSettings `json:"royale"`
SquadSettings SquadSettings `json:"squad"`

seed int64
rand Rand
}

// RoyaleSettings contains settings that are specific to the "royale" game mode
Expand All @@ -267,6 +273,28 @@ type SquadSettings struct {
SharedLength bool `json:"sharedLength"`
}

// Retrieve a random number generator based on a fixed seed.
// The random number generator is cached in the BoardState, allowing it to be overridden for tests.
func (s Settings) Rand() Rand {
if s.rand != nil {
return s.rand
}
s.rand = rand.New(rand.NewSource(s.seed))
return s.rand
}

// Override the built in random number generator for this BoardState.
// For use in testing to make the game deterministic.
func (s Settings) SetRand(rand Rand) {
s.rand = rand
}

// Set the BoardState's seed, which is used to generate random numbers.
func (s Settings) SetSeed(seed int64) {
s.seed = seed
s.rand = nil
}

// StageFunc represents a single stage of an ordered pipeline and applies custom logic to the board state each turn.
// It is expected to modify the boardState directly.
// The return values are a boolean (to indicate whether the game has ended as a result of the stage)
Expand Down
31 changes: 29 additions & 2 deletions standard.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package rules

import (
"math/rand"
"sort"
)

Expand All @@ -11,6 +10,8 @@ type StandardRuleset struct {
HazardDamagePerTurn int32
HazardMap string // optional
HazardMapAuthor string // optional

seed int64
}

var standardRulesetStages = []string{
Expand Down Expand Up @@ -395,7 +396,7 @@ func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (boo
if numCurrentFood < settings.MinimumFood {
return false, PlaceFoodRandomly(b, settings.MinimumFood-numCurrentFood)
}
if settings.FoodSpawnChance > 0 && int32(rand.Intn(100)) < settings.FoodSpawnChance {
if settings.FoodSpawnChance > 0 && int32(settings.Rand().Intn(100)) < settings.FoodSpawnChance {
return false, PlaceFoodRandomly(b, 1)
}
return false, nil
Expand All @@ -422,6 +423,7 @@ func (r StandardRuleset) Settings() Settings {
HazardDamagePerTurn: r.HazardDamagePerTurn,
HazardMap: r.HazardMap,
HazardMapAuthor: r.HazardMapAuthor,
seed: r.seed,
}
}

Expand All @@ -436,3 +438,28 @@ func IsInitialization(b *BoardState, settings Settings, moves []SnakeMove) bool
// the turn hasn't advanced and the moves are empty
return b.Turn <= 0 && len(moves) == 0
}

func InitializeBoardStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
// Only activate in initialization
if !IsInitialization(b, settings, moves) {
return false, nil
}

if err := placeFoodAutomaticallyWithRand(settings.Rand(), b); err != nil {
return false, err
}
snakeIDs := make([]string, 0, len(b.Snakes))
for _, snake := range b.Snakes {
// skip placing all snakes automatically if any have a body
if len(snake.Body) > 0 {
return false, nil
}
snakeIDs = append(snakeIDs, snake.ID)
}

if err := placeSnakesAutomaticallyWithRand(settings.Rand(), b, snakeIDs); err != nil {
return false, err
}

return false, nil
}
Loading

0 comments on commit 5fc70a7

Please sign in to comment.