diff --git a/royale.go b/royale.go index 24d2c4f..1638bc1 100644 --- a/royale.go +++ b/royale.go @@ -2,6 +2,8 @@ package rules import ( "errors" + "hash/crc32" + "math/rand" ) type RoyaleRuleset struct { @@ -63,9 +65,27 @@ func (r *RoyaleRuleset) populateOutOfBounds(b *BoardState, turn int32) error { return nil } + randGenerator, err := r.getRandGenerator(b) + if err != nil { + return err + } + numShrinks := turn / r.ShrinkEveryNTurns - minX, maxX := numShrinks, b.Width-1-numShrinks - minY, maxY := numShrinks, b.Height-1-numShrinks + minX, maxX := int32(0), b.Width-1 + minY, maxY := int32(0), b.Height-1 + for i := int32(0); i < numShrinks; i++ { + switch randGenerator.Intn(4) { + case 0: + minX += 1 + case 1: + maxX -= 1 + case 2: + minY += 1 + case 3: + maxY -= 1 + } + } + for x := int32(0); x < b.Width; x++ { for y := int32(0); y < b.Height; y++ { if x < minX || x > maxX || y < minY || y > maxY { @@ -101,3 +121,21 @@ func (r *RoyaleRuleset) damageOutOfBounds(b *BoardState) error { return nil } + +func (r *RoyaleRuleset) getRandGenerator(b *BoardState) (*rand.Rand, error) { + if len(b.Snakes) < 1 { + return nil, errors.New("royale mode requires at least one snake id") + } + + // Use the "lowest" Snake ID as a random seed + seedStr := b.Snakes[0].ID + for i := 1; i < len(b.Snakes); i++ { + if b.Snakes[i].ID < seedStr { + seedStr = b.Snakes[i].ID + } + } + + seed := int64(crc32.ChecksumIEEE([]byte(seedStr))) + + return rand.New(rand.NewSource(seed)), nil +} diff --git a/royale_test.go b/royale_test.go index 1b4ad95..6ff0b4c 100644 --- a/royale_test.go +++ b/royale_test.go @@ -23,6 +23,17 @@ func TestRoyaleDefaultSanity(t *testing.T) { require.NoError(t, err) } +func TestRoyaleOutOfBoundsNoSnakes(t *testing.T) { + b := &BoardState{} + r := RoyaleRuleset{ + ShrinkEveryNTurns: 10, + DamagePerTurn: 10, + } + + err := r.populateOutOfBounds(b, 100) + require.Equal(t, errors.New("royale mode requires at least one snake id"), err) +} + func TestRoyaleOutOfBounds(t *testing.T) { tests := []struct { Width int32 @@ -39,24 +50,48 @@ func TestRoyaleOutOfBounds(t *testing.T) { {Width: 3, Height: 3, Turn: 9, ShrinkEveryNTurns: 10, ExpectedOutOfBounds: []Point{}}, { Width: 3, Height: 3, Turn: 10, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}}, }, { Width: 3, Height: 3, Turn: 11, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}}, }, { Width: 3, Height: 3, Turn: 19, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}}, }, { Width: 3, Height: 3, Turn: 20, ShrinkEveryNTurns: 10, + ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 2}, {2, 2}}, + }, + { + Width: 3, Height: 3, Turn: 31, ShrinkEveryNTurns: 10, + ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 2}}, + }, + { + Width: 3, Height: 3, Turn: 42, ShrinkEveryNTurns: 10, + ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 2}}, + }, + { + Width: 3, Height: 3, Turn: 53, ShrinkEveryNTurns: 10, + ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + }, + { + Width: 3, Height: 3, Turn: 64, ShrinkEveryNTurns: 10, + ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + }, + { + Width: 3, Height: 3, Turn: 6987, ShrinkEveryNTurns: 10, ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, }, } for _, test := range tests { - b := &BoardState{Width: test.Width, Height: test.Height} + b := &BoardState{ + Width: test.Width, + Height: test.Height, + Snakes: []Snake{{ID: "test-snake"}}, + } r := RoyaleRuleset{ Turn: test.Turn, ShrinkEveryNTurns: test.ShrinkEveryNTurns, @@ -174,38 +209,68 @@ func TestRoyaleDamagePerTurn(t *testing.T) { } func TestRoyalDamageNextTurn(t *testing.T) { - b := &BoardState{Width: 10, Height: 10, Snakes: []Snake{{ID: "one", Health: 100, Body: []Point{{1, 1}}}}} - r := RoyaleRuleset{Turn: 10, ShrinkEveryNTurns: 10, DamagePerTurn: 30} + b := &BoardState{Width: 10, Height: 10, Snakes: []Snake{{ID: "one", Health: 100, Body: []Point{{1, 0}}}}} + r := RoyaleRuleset{ShrinkEveryNTurns: 10, DamagePerTurn: 30} m := []SnakeMove{{ID: "one", Move: "right"}} + r.Turn = 10 n, err := r.CreateNextBoardState(b, m) require.NoError(t, err) require.Equal(t, NotEliminated, n.Snakes[0].EliminatedCause) require.Equal(t, int32(99), n.Snakes[0].Health) - require.Equal(t, Point{2, 1}, n.Snakes[0].Body[0]) - require.Equal(t, 36, len(r.OutOfBounds)) + require.Equal(t, Point{2, 0}, n.Snakes[0].Body[0]) + require.Equal(t, 10, len(r.OutOfBounds)) // X = 0 r.Turn = 20 n, err = r.CreateNextBoardState(b, m) require.NoError(t, err) require.Equal(t, NotEliminated, n.Snakes[0].EliminatedCause) require.Equal(t, int32(99), n.Snakes[0].Health) - require.Equal(t, Point{2, 1}, n.Snakes[0].Body[0]) - require.Equal(t, 64, len(r.OutOfBounds)) + require.Equal(t, Point{2, 0}, n.Snakes[0].Body[0]) + require.Equal(t, 19, len(r.OutOfBounds)) // Y = 0 r.Turn = 21 n, err = r.CreateNextBoardState(b, m) require.NoError(t, err) require.Equal(t, NotEliminated, n.Snakes[0].EliminatedCause) require.Equal(t, int32(69), n.Snakes[0].Health) - require.Equal(t, Point{2, 1}, n.Snakes[0].Body[0]) - require.Equal(t, 64, len(r.OutOfBounds)) + require.Equal(t, Point{2, 0}, n.Snakes[0].Body[0]) + require.Equal(t, 19, len(r.OutOfBounds)) b.Snakes[0].Health = 15 n, err = r.CreateNextBoardState(b, m) require.NoError(t, err) require.Equal(t, EliminatedByStarvation, n.Snakes[0].EliminatedCause) require.Equal(t, int32(0), n.Snakes[0].Health) - require.Equal(t, Point{2, 1}, n.Snakes[0].Body[0]) - require.Equal(t, 64, len(r.OutOfBounds)) + require.Equal(t, Point{2, 0}, n.Snakes[0].Body[0]) + require.Equal(t, 19, len(r.OutOfBounds)) +} + +func TestRoyalGetRandGenerator(t *testing.T) { + tests := []struct { + SnakeIDs []string + Error error + firstInt int + }{ + {[]string{}, errors.New("royale mode requires at least one snake id"), 0}, + {[]string{"1"}, nil, 1400170195406563237}, + {[]string{"1", "2", "3", "4", "5"}, nil, 1400170195406563237}, + {[]string{"5", "4", "3", "2", "1"}, nil, 1400170195406563237}, + {[]string{"3", "4", "1", "5", "2"}, nil, 1400170195406563237}, + {[]string{"3", "4", "5", "2"}, nil, 5139088052943840554}, + } + + for _, test := range tests { + b := &BoardState{} + for _, id := range test.SnakeIDs { + b.Snakes = append(b.Snakes, Snake{ID: id}) + } + + r := RoyaleRuleset{} + generator, err := r.getRandGenerator(b) + require.Equal(t, test.Error, err) + if err == nil { + require.Equal(t, test.firstInt, generator.Int()) + } + } }