DEV-280: Extract board generation out of rules.Ruleset (#51)
* extract board generation out of rules.Ruleset * update comment and remove redundant interface check * clone boardState in constrictor to respect the ModifyBoardState interface
This commit is contained in:
parent
e416384007
commit
015b681f14
8 changed files with 1006 additions and 917 deletions
257
standard.go
257
standard.go
|
|
@ -13,182 +13,14 @@ type StandardRuleset struct {
|
|||
|
||||
func (r *StandardRuleset) Name() string { return "standard" }
|
||||
|
||||
func (r *StandardRuleset) CreateInitialBoardState(width int32, height int32, snakeIDs []string) (*BoardState, error) {
|
||||
initialBoardState := &BoardState{
|
||||
Height: height,
|
||||
Width: width,
|
||||
Snakes: make([]Snake, len(snakeIDs)),
|
||||
}
|
||||
|
||||
for i := 0; i < len(snakeIDs); i++ {
|
||||
initialBoardState.Snakes[i] = Snake{
|
||||
ID: snakeIDs[i],
|
||||
Health: SnakeMaxHealth,
|
||||
}
|
||||
}
|
||||
|
||||
err := r.placeSnakes(initialBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.placeFood(initialBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return initialBoardState, nil
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) placeSnakes(b *BoardState) error {
|
||||
if r.isKnownBoardSize(b) {
|
||||
return r.placeSnakesFixed(b)
|
||||
}
|
||||
return r.placeSnakesRandomly(b)
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) placeSnakesFixed(b *BoardState) error {
|
||||
// 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 (r *StandardRuleset) placeSnakesRandomly(b *BoardState) error {
|
||||
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
unoccupiedPoints := r.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
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) placeFood(b *BoardState) error {
|
||||
if r.isKnownBoardSize(b) {
|
||||
return r.placeFoodFixed(b)
|
||||
}
|
||||
return r.placeFoodRandomly(b)
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) placeFoodFixed(b *BoardState) error {
|
||||
// Place 1 food within exactly 2 moves of each snake
|
||||
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},
|
||||
}
|
||||
availableFoodLocations := []Point{}
|
||||
|
||||
for _, p := range possibleFoodLocations {
|
||||
isOccupiedAlready := false
|
||||
for _, food := range b.Food {
|
||||
if food.X == p.X && food.Y == p.Y {
|
||||
isOccupiedAlready = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isOccupiedAlready {
|
||||
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
|
||||
centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2}
|
||||
unoccupiedPoints := r.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
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) placeFoodRandomly(b *BoardState) error {
|
||||
return r.spawnFood(b, int32(len(b.Snakes)))
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) isKnownBoardSize(b *BoardState) bool {
|
||||
if b.Height == BoardSizeSmall && b.Width == BoardSizeSmall {
|
||||
return true
|
||||
}
|
||||
if b.Height == BoardSizeMedium && b.Width == BoardSizeMedium {
|
||||
return true
|
||||
}
|
||||
if b.Height == BoardSizeLarge && b.Width == BoardSizeLarge {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
|
||||
// No-op
|
||||
return initialState, nil
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
// We specifically want to copy prevState, so as not to alter it directly.
|
||||
nextState := &BoardState{
|
||||
Height: prevState.Height,
|
||||
Width: prevState.Width,
|
||||
Food: append([]Point{}, prevState.Food...),
|
||||
Snakes: make([]Snake, len(prevState.Snakes)),
|
||||
Hazards: append([]Point{}, prevState.Hazards...),
|
||||
}
|
||||
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].EliminatedCause = prevState.Snakes[i].EliminatedCause
|
||||
nextState.Snakes[i].EliminatedBy = prevState.Snakes[i].EliminatedBy
|
||||
}
|
||||
nextState := prevState.Clone()
|
||||
|
||||
// TODO: Gut check the BoardState?
|
||||
|
||||
|
|
@ -545,90 +377,13 @@ func (r *StandardRuleset) growSnake(snake *Snake) {
|
|||
func (r *StandardRuleset) maybeSpawnFood(b *BoardState) error {
|
||||
numCurrentFood := int32(len(b.Food))
|
||||
if numCurrentFood < r.MinimumFood {
|
||||
return r.spawnFood(b, r.MinimumFood-numCurrentFood)
|
||||
return PlaceFoodRandomly(b, r.MinimumFood-numCurrentFood)
|
||||
} else if r.FoodSpawnChance > 0 && int32(rand.Intn(100)) < r.FoodSpawnChance {
|
||||
return r.spawnFood(b, 1)
|
||||
return PlaceFoodRandomly(b, 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) spawnFood(b *BoardState, n int32) error {
|
||||
for i := int32(0); i < n; i++ {
|
||||
unoccupiedPoints := r.getUnoccupiedPoints(b, false)
|
||||
if len(unoccupiedPoints) > 0 {
|
||||
newFood := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
|
||||
b.Food = append(b.Food, newFood)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) getUnoccupiedPoints(b *BoardState, includePossibleMoves bool) []Point {
|
||||
pointIsOccupied := map[int32]map[int32]bool{}
|
||||
for _, p := range b.Food {
|
||||
if _, xExists := pointIsOccupied[p.X]; !xExists {
|
||||
pointIsOccupied[p.X] = map[int32]bool{}
|
||||
}
|
||||
pointIsOccupied[p.X][p.Y] = true
|
||||
}
|
||||
for _, snake := range b.Snakes {
|
||||
if snake.EliminatedCause != NotEliminated {
|
||||
continue
|
||||
}
|
||||
for i, p := range snake.Body {
|
||||
if _, xExists := pointIsOccupied[p.X]; !xExists {
|
||||
pointIsOccupied[p.X] = map[int32]bool{}
|
||||
}
|
||||
pointIsOccupied[p.X][p.Y] = true
|
||||
|
||||
if i == 0 && !includePossibleMoves {
|
||||
nextMovePoints := []Point{
|
||||
{X: p.X - 1, Y: p.Y},
|
||||
{X: p.X + 1, Y: p.Y},
|
||||
{X: p.X, Y: p.Y - 1},
|
||||
{X: p.X, Y: p.Y + 1},
|
||||
}
|
||||
for _, nextP := range nextMovePoints {
|
||||
if _, xExists := pointIsOccupied[nextP.X]; !xExists {
|
||||
pointIsOccupied[nextP.X] = map[int32]bool{}
|
||||
}
|
||||
pointIsOccupied[nextP.X][nextP.Y] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unoccupiedPoints := []Point{}
|
||||
for x := int32(0); x < b.Width; x++ {
|
||||
for y := int32(0); y < b.Height; y++ {
|
||||
if _, xExists := pointIsOccupied[x]; xExists {
|
||||
if isOccupied, yExists := pointIsOccupied[x][y]; yExists {
|
||||
if isOccupied {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
unoccupiedPoints = append(unoccupiedPoints, Point{X: x, Y: y})
|
||||
}
|
||||
}
|
||||
return unoccupiedPoints
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) getEvenUnoccupiedPoints(b *BoardState) []Point {
|
||||
// Start by getting unoccupied points
|
||||
unoccupiedPoints := r.getUnoccupiedPoints(b, true)
|
||||
|
||||
// Create a new array to hold points that are even
|
||||
evenUnoccupiedPoints := []Point{}
|
||||
|
||||
for _, point := range unoccupiedPoints {
|
||||
if ((point.X + point.Y) % 2) == 0 {
|
||||
evenUnoccupiedPoints = append(evenUnoccupiedPoints, point)
|
||||
}
|
||||
}
|
||||
return evenUnoccupiedPoints
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
numSnakesRemaining := 0
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue