From fcc3c348bf9704e0143cc9b96815d9a95e0908cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:01:33 +0000 Subject: [PATCH 1/2] Bump golang.org/x/net from 0.23.0 to 0.38.0 Bumps [golang.org/x/net](https://github.com/golang/net) from 0.23.0 to 0.38.0. - [Commits](https://github.com/golang/net/compare/v0.23.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.38.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 8 ++++---- go.sum | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 7bc7fa8..5c9ce13 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/BattlesnakeOfficial/rules -go 1.21 +go 1.23.0 require ( github.com/google/uuid v1.5.0 @@ -32,9 +32,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 89f4674..8bd9aa9 100644 --- a/go.sum +++ b/go.sum @@ -71,13 +71,13 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From e84710273936c2eee82b8764c0e8f86a6fd75212 Mon Sep 17 00:00:00 2001 From: Justin Workman Date: Wed, 24 Dec 2025 10:33:19 -0800 Subject: [PATCH 2/2] Update Golang to 1.23 and fix lints to unblock dependabot updates --- .devcontainer/devcontainer.json | 6 ++-- .gitattributes | 4 +++ .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- .golangci.yaml | 2 ++ Makefile | 4 +-- board_test.go | 4 +-- cli/commands/play.go | 62 +++++++++++++++++++-------------- cli/commands/play_test.go | 3 +- maps/hazard_pits.go | 3 +- maps/hazard_pits_test.go | 17 ++++----- maps/hazards.go | 9 ++--- maps/sinkholes.go | 17 +++++---- rand.go | 47 +++++++++++++++++++------ royale_test.go | 3 +- ruleset_test.go | 8 ++--- standard.go | 7 ++-- standard_test.go | 11 +++--- test/test_utils.go | 6 ++-- 19 files changed, 128 insertions(+), 89 deletions(-) create mode 100644 .gitattributes diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d621336..7116764 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ { "name": "rules.battlesnake.local", - "image": "mcr.microsoft.com/devcontainers/go:1.21", - "postCreateCommand": "go mod download", + "image": "mcr.microsoft.com/devcontainers/go:1.23", + "postCreateCommand": "sh -c \"go mod download && git config --global --add safe.directory /workspaces/rules\"", "forwardPorts": [], "customizations": { "vscode": { @@ -14,4 +14,4 @@ } } } -} +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..69587a6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf + +# Keep Windows-only scripts with CRLF if they ever appear +*.bat text eol=crlf \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eecf51e..128af74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: branches: [main] env: - GO_VERSION: '1.20' + GO_VERSION: '1.23' jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e5fbdc3..a9eed50 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: workflow_call: # Allow other workflows to call this one env: - GO_VERSION: '1.20' + GO_VERSION: '1.23' jobs: diff --git a/.golangci.yaml b/.golangci.yaml index 89bdde5..83b3108 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,3 +1,5 @@ +version: 2 + issues: exclude-rules: - linters: diff --git a/Makefile b/Makefile index e7fe787..0d48940 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ GOPATH := $(shell go env GOPATH) GOLANGCI_LINT_PATH := ${GOPATH}/bin/golangci-lint -GOLANGCI_LINT_VERSION := 1.55.2 +GOLANGCI_LINT_VERSION := 2.2.1 ${GOLANGCI_LINT_PATH}: @@ -12,7 +12,7 @@ install-cli: .PHONY: install-cli test-format: - test -z $$(gofmt -l .) || (gofmt -l . && exit 1) + @files=$$(gofmt -l .); if [ -n "$$files" ]; then echo "$$files"; exit 1; fi .PHONY: test-format test-lint: ${GOLANGCI_LINT_PATH} diff --git a/board_test.go b/board_test.go index 827bb4f..827c877 100644 --- a/board_test.go +++ b/board_test.go @@ -299,8 +299,8 @@ func TestPlaceSnakesDefault(t *testing.T) { } // All snakes are expected to be placed on an even square - this is true even of fixed positions for known board sizes - var snakePlacedOnEvenSquare bool = ((test.BoardState.Snakes[i].Body[0].X + test.BoardState.Snakes[i].Body[0].Y) % 2) == 0 - require.Equal(t, true, snakePlacedOnEvenSquare) + snakePlacedOnEvenSquare := ((test.BoardState.Snakes[i].Body[0].X + test.BoardState.Snakes[i].Body[0].Y) % 2) == 0 + require.True(t, snakePlacedOnEvenSquare) } } }) diff --git a/cli/commands/play.go b/cli/commands/play.go index caabe2d..b542501 100644 --- a/cli/commands/play.go +++ b/cli/commands/play.go @@ -5,8 +5,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" - "math/rand" "net/http" "net/url" "os" @@ -139,7 +137,7 @@ func (gameState *GameState) Initialize() error { // Load game map gameMap, err := maps.GetMap(gameState.MapName) if err != nil { - return fmt.Errorf("Failed to load game map %#v: %v", gameState.MapName, err) + return fmt.Errorf("failed to load game map %#v: %v", gameState.MapName, err) } gameState.gameMap = gameMap @@ -165,7 +163,7 @@ func (gameState *GameState) Initialize() error { if gameState.OutputPath != "" { f, err := os.OpenFile(gameState.OutputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { - return fmt.Errorf("Failed to open output file: %w", err) + return fmt.Errorf("failed to open output file: %w", err) } gameState.outputFile = f } @@ -181,14 +179,14 @@ func (gameState *GameState) Run() error { // Setup local state for snakes gameState.snakeStates, err = gameState.buildSnakesFromOptions() if err != nil { - return fmt.Errorf("Error getting snake metadata: %w", err) + return fmt.Errorf("error getting snake metadata: %w", err) } - rand.Seed(gameState.Seed) + rules.SetGlobalSeed(gameState.Seed) gameOver, boardState, err := gameState.initializeBoardFromArgs() if err != nil { - return fmt.Errorf("Error initializing board: %w", err) + return fmt.Errorf("error initializing board: %w", err) } gameExporter := GameExporter{ @@ -199,7 +197,11 @@ func (gameState *GameState) Run() error { } exportGame := gameState.outputFile != nil if exportGame { - defer gameState.outputFile.Close() + defer func() { + if err := gameState.outputFile.Close(); err != nil { + log.WARN.Printf("error closing output file: %v", err) + } + }() } boardGame := board.Game{ @@ -219,7 +221,7 @@ func (gameState *GameState) Run() error { if gameState.ViewInBrowser { serverURL, err := boardServer.Listen() if err != nil { - return fmt.Errorf("Error starting HTTP server: %w", err) + return fmt.Errorf("error starting HTTP server: %w", err) } defer boardServer.Shutdown() log.INFO.Printf("Board server listening on %s", serverURL) @@ -267,7 +269,7 @@ func (gameState *GameState) Run() error { gameOver, boardState, err = gameState.createNextBoardState(boardState) if err != nil { - return fmt.Errorf("Error processing game: %w", err) + return fmt.Errorf("error processing game: %w", err) } if gameOver { @@ -337,7 +339,7 @@ func (gameState *GameState) Run() error { if exportGame { lines, err := gameExporter.FlushToFile(gameState.outputFile) if err != nil { - return fmt.Errorf("Unable to export game: %w", err) + return fmt.Errorf("unable to export game: %w", err) } log.INFO.Printf("Wrote %d lines to output file: %s", lines, gameState.OutputPath) } @@ -352,11 +354,11 @@ func (gameState *GameState) initializeBoardFromArgs() (bool, *rules.BoardState, } boardState, err := maps.SetupBoard(gameState.gameMap.ID(), gameState.ruleset.Settings(), gameState.Width, gameState.Height, snakeIds) if err != nil { - return false, nil, fmt.Errorf("Error initializing BoardState with map: %w", err) + return false, nil, fmt.Errorf("error initializing BoardState with map: %w", err) } gameOver, boardState, err := gameState.ruleset.Execute(boardState, nil) if err != nil { - return false, nil, fmt.Errorf("Error initializing BoardState with ruleset: %w", err) + return false, nil, fmt.Errorf("error initializing BoardState with ruleset: %w", err) } for _, snakeState := range gameState.snakeStates { @@ -377,7 +379,7 @@ func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) ( // apply PreUpdateBoard before making requests to snakes boardState, err := maps.PreUpdateBoard(gameState.gameMap, boardState, gameState.ruleset.Settings()) if err != nil { - return false, boardState, fmt.Errorf("Error pre-updating board with game map: %w", err) + return false, boardState, fmt.Errorf("error pre-updating board with game map: %w", err) } // get moves from snakes @@ -420,13 +422,13 @@ func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) ( gameOver, boardState, err := gameState.ruleset.Execute(boardState, moves) if err != nil { - return false, boardState, fmt.Errorf("Error updating board state from ruleset: %w", err) + return false, boardState, fmt.Errorf("error updating board state from ruleset: %w", err) } // apply PostUpdateBoard after ruleset operates on snake moves boardState, err = maps.PostUpdateBoard(gameState.gameMap, boardState, gameState.ruleset.Settings()) if err != nil { - return false, boardState, fmt.Errorf("Error post-updating board with game map: %w", err) + return false, boardState, fmt.Errorf("error post-updating board with game map: %w", err) } boardState.Turn += 1 @@ -470,8 +472,12 @@ func (gameState *GameState) getSnakeUpdate(boardState *rules.BoardState, snakeSt "\tError: body is empty", u.String()) return snakeState } - defer res.Body.Close() - body, readErr := ioutil.ReadAll(res.Body) + defer func() { + if err := res.Body.Close(); err != nil { + log.WARN.Printf("error closing response body from %v: %v", u.String(), err) + } + }() + body, readErr := io.ReadAll(res.Body) if readErr != nil { log.WARN.Printf( "Failed to read response body from %v\n"+ @@ -586,11 +592,11 @@ func (gameState *GameState) buildSnakesFromOptions() (map[string]SnakeState, err if i < numURLs { u, err := url.ParseRequestURI(gameState.URLs[i]) if err != nil { - return nil, fmt.Errorf("URL %v is not valid: %w", gameState.URLs[i], err) + return nil, fmt.Errorf("invalid URL: %v: %w", gameState.URLs[i], err) } snakeURL = u.String() } else { - return nil, fmt.Errorf("URL for name %v is missing", gameState.Names[i]) + return nil, fmt.Errorf("missing URL for name: %v", gameState.Names[i]) } snakeState := SnakeState{ @@ -599,25 +605,29 @@ func (gameState *GameState) buildSnakesFromOptions() (map[string]SnakeState, err var snakeErr error res, _, err := gameState.httpClient.Get(snakeURL) if err != nil { - return nil, fmt.Errorf("Snake metadata request to %v failed: %w", snakeURL, err) + return nil, fmt.Errorf("snake metadata request to %v failed: %w", snakeURL, err) } snakeState.StatusCode = res.StatusCode if res.Body == nil { - return nil, fmt.Errorf("Empty response body from snake metadata URL: %v", snakeURL) + return nil, fmt.Errorf("empty response body from snake metadata URL: %v", snakeURL) } - defer res.Body.Close() - body, readErr := ioutil.ReadAll(res.Body) + defer func() { + if err := res.Body.Close(); err != nil { + log.WARN.Printf("error closing snake metadata response from %v: %v", snakeURL, err) + } + }() + body, readErr := io.ReadAll(res.Body) if readErr != nil { - return nil, fmt.Errorf("Error reading from snake metadata URL %v: %w", snakeURL, readErr) + return nil, fmt.Errorf("error reading from snake metadata URL %v: %w", snakeURL, readErr) } pingResponse := client.SnakeMetadataResponse{} jsonErr := json.Unmarshal(body, &pingResponse) if jsonErr != nil { - return nil, fmt.Errorf("Failed to parse response from %v: %w", snakeURL, jsonErr) + return nil, fmt.Errorf("failed to parse response from %v: %w", snakeURL, jsonErr) } snakeState.Head = pingResponse.Head diff --git a/cli/commands/play_test.go b/cli/commands/play_test.go index 44d0b35..d04e7f0 100644 --- a/cli/commands/play_test.go +++ b/cli/commands/play_test.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "strings" "testing" @@ -629,7 +628,7 @@ func (client stubHTTPClient) request(url string) (*http.Response, time.Duration, if client.err != nil { return nil, client.latency, client.err } - body := ioutil.NopCloser(bytes.NewBufferString(client.body(url))) + body := io.NopCloser(bytes.NewBufferString(client.body(url))) response := &http.Response{ Header: make(http.Header), diff --git a/maps/hazard_pits.go b/maps/hazard_pits.go index fa56337..e5ed1ae 100644 --- a/maps/hazard_pits.go +++ b/maps/hazard_pits.go @@ -63,6 +63,7 @@ func (m HazardPitsMap) SetupBoard(initialBoardState *rules.BoardState, settings tempBoardState := rules.NewBoardState(initialBoardState.Width, initialBoardState.Height) tempBoardState.Snakes = make([]rules.Snake, len(snakeIDs)) + tempEditor := NewBoardStateEditor(tempBoardState) for i := 0; i < len(snakeIDs); i++ { tempBoardState.Snakes[i] = rules.Snake{ @@ -79,7 +80,7 @@ func (m HazardPitsMap) SetupBoard(initialBoardState *rules.BoardState, settings } } - err := rules.PlaceFoodFixed(rand, tempBoardState) + err := PlaceFoodFixed(rand, tempBoardState, tempEditor) if err != nil { return err } diff --git a/maps/hazard_pits_test.go b/maps/hazard_pits_test.go index dba6a9a..713c2ea 100644 --- a/maps/hazard_pits_test.go +++ b/maps/hazard_pits_test.go @@ -49,21 +49,18 @@ func TestHazardPitsMap(t *testing.T) { state.Turn = i err = m.PostUpdateBoard(state, settings, editor) require.NoError(t, err) - if i == 1 { + switch i { + case 1: require.Len(t, state.Hazards, 21) - } else if i == 2 { + case 2: require.Len(t, state.Hazards, 42) - } else if i == 3 { + case 3: require.Len(t, state.Hazards, 63) - } else if i == 4 { + case 4, 5, 6: require.Len(t, state.Hazards, 84) - } else if i == 5 { - require.Len(t, state.Hazards, 84) - } else if i == 6 { - require.Len(t, state.Hazards, 84) - } else if i == 7 { + case 7: require.Len(t, state.Hazards, 0) - } else if i == 8 { + case 8: require.Len(t, state.Hazards, 21) } } diff --git a/maps/hazards.go b/maps/hazards.go index f4db547..2edc623 100644 --- a/maps/hazards.go +++ b/maps/hazards.go @@ -388,7 +388,8 @@ func (m DirectionalExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardS addHazards := i == maxTurns-1 - if growthDirection == "left" { + switch growthDirection { + case "left": x := topLeft.X - 1 if addHazards { for y := bottomRight.Y; y < topLeft.Y+1; y++ { @@ -396,7 +397,7 @@ func (m DirectionalExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardS } } topLeft.X = x - } else if growthDirection == "right" { + case "right": x := bottomRight.X + 1 if addHazards { for y := bottomRight.Y; y < topLeft.Y+1; y++ { @@ -404,7 +405,7 @@ func (m DirectionalExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardS } } bottomRight.X = x - } else if growthDirection == "up" { + case "up": y := topLeft.Y + 1 if addHazards { for x := topLeft.X; x < bottomRight.X+1; x++ { @@ -412,7 +413,7 @@ func (m DirectionalExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardS } } topLeft.Y = y - } else if growthDirection == "down" { + case "down": y := bottomRight.Y - 1 if addHazards { for x := topLeft.X; x < bottomRight.X+1; x++ { diff --git a/maps/sinkholes.go b/maps/sinkholes.go index 25990d9..ab0c1f0 100644 --- a/maps/sinkholes.go +++ b/maps/sinkholes.go @@ -51,9 +51,10 @@ func (m SinkholesMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings spawnEveryNTurns = shrinkEveryNTurns } maxRings := 5 - if lastBoardState.Width == 7 { + switch lastBoardState.Width { + case 7: maxRings = 3 - } else if lastBoardState.Width == 19 { + case 19: maxRings = 7 } @@ -79,13 +80,15 @@ func (m SinkholesMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings if offset > 0 && offset <= maxRings { for x := spawnLocation.X - offset; x <= spawnLocation.X+offset; x++ { for y := spawnLocation.Y - offset; y <= spawnLocation.Y+offset; y++ { + isCorner := (x == spawnLocation.X-offset && y == spawnLocation.Y-offset) || + (x == spawnLocation.X+offset && y == spawnLocation.Y-offset) || + (x == spawnLocation.X-offset && y == spawnLocation.Y+offset) || + (x == spawnLocation.X+offset && y == spawnLocation.Y+offset) // don't draw in the corners of the square so we get a rounded effect - if !(x == spawnLocation.X-offset && y == spawnLocation.Y-offset) && - !(x == spawnLocation.X+offset && y == spawnLocation.Y-offset) && - !(x == spawnLocation.X-offset && y == spawnLocation.Y+offset) && - !(x == spawnLocation.X+offset && y == spawnLocation.Y+offset) { - editor.AddHazard(rules.Point{X: x, Y: y}) + if isCorner { + continue } + editor.AddHazard(rules.Point{X: x, Y: y}) } } } diff --git a/rand.go b/rand.go index acbfcaf..6c14281 100644 --- a/rand.go +++ b/rand.go @@ -1,6 +1,9 @@ package rules -import "math/rand" +import ( + "math/rand" + "sync" +) type Rand interface { Intn(n int) int @@ -11,21 +14,45 @@ type Rand interface { Shuffle(n int, swap func(i, j int)) } -// A Rand implementation that just uses the global math/rand generator. -var GlobalRand globalRand +// A Rand implementation that just uses a shared math/rand.Rand guarded by a lock. +// This preserves the behaviour of the package-level math/rand global while allowing +// deterministic seeding without rand.Seed. +var GlobalRand Rand = globalRandInstance() -type globalRand struct{} +type lockedRand struct { + mu sync.Mutex + r *rand.Rand +} + +func globalRandInstance() *lockedRand { + return &lockedRand{r: rand.New(rand.NewSource(1))} +} + +func (g *lockedRand) Range(min, max int) int { + g.mu.Lock() + defer g.mu.Unlock() + return g.r.Intn(max-min+1) + min +} -func (globalRand) Range(min, max int) int { - return rand.Intn(max-min+1) + min +func (g *lockedRand) Intn(n int) int { + g.mu.Lock() + defer g.mu.Unlock() + return g.r.Intn(n) } -func (globalRand) Intn(n int) int { - return rand.Intn(n) +func (g *lockedRand) Shuffle(n int, swap func(i, j int)) { + g.mu.Lock() + defer g.mu.Unlock() + g.r.Shuffle(n, swap) } -func (globalRand) Shuffle(n int, swap func(i, j int)) { - rand.Shuffle(n, swap) +// SetGlobalSeed seeds the shared global RNG using rand.New(rand.NewSource(seed)), +// matching the previous rand.Seed semantics without triggering deprecation lints. +func SetGlobalSeed(seed int64) { + gr := GlobalRand.(*lockedRand) + gr.mu.Lock() + gr.r = rand.New(rand.NewSource(seed)) + gr.mu.Unlock() } type seedRand struct { diff --git a/royale_test.go b/royale_test.go index c08fe5b..e6550a9 100644 --- a/royale_test.go +++ b/royale_test.go @@ -3,7 +3,6 @@ package rules import ( "errors" "fmt" - "math/rand" "testing" "github.com/stretchr/testify/require" @@ -207,7 +206,7 @@ func TestRoyaleCreateNextBoardState(t *testing.T) { ParamShrinkEveryNTurns: "1", }).WithSeed(1234) for _, gc := range cases { - rand.Seed(1234) + SetGlobalSeed(1234) // test a RulesBuilder constructed instance gc.requireValidNextState(t, rb.NamedRuleset(GameTypeRoyale)) // also test a pipeline with the same settings diff --git a/ruleset_test.go b/ruleset_test.go index ec2d958..b528cd1 100644 --- a/ruleset_test.go +++ b/ruleset_test.go @@ -129,11 +129,9 @@ func TestRulesetBuilderGameOver(t *testing.T) { } func TestStageFuncContract(t *testing.T) { - //nolint:gosimple - var stage rules.StageFunc - stage = func(bs *rules.BoardState, s rules.Settings, sm []rules.SnakeMove) (bool, error) { + stage := rules.StageFunc(func(bs *rules.BoardState, s rules.Settings, sm []rules.SnakeMove) (bool, error) { return true, nil - } + }) ended, err := stage(nil, rules.NewRulesetBuilder().NamedRuleset("").Settings(), nil) require.NoError(t, err) require.True(t, ended) @@ -141,7 +139,7 @@ func TestStageFuncContract(t *testing.T) { func TestRulesetBuilderGetRand(t *testing.T) { var seed int64 = 12345 - var turn int = 5 + turn := 5 ruleset := rules.NewRulesetBuilder().WithSeed(seed).PipelineRuleset("example", rules.NewPipeline(rules.StageGameOverStandard)) rand1 := ruleset.Settings().GetRand(turn) diff --git a/standard.go b/standard.go index 3312ac5..17be778 100644 --- a/standard.go +++ b/standard.go @@ -1,9 +1,6 @@ package rules -import ( - "math/rand" - "sort" -) +import "sort" var standardRulesetStages = []string{ StageGameOverStandard, @@ -375,7 +372,7 @@ func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (boo if numCurrentFood < minimumFood { return false, PlaceFoodRandomly(GlobalRand, b, minimumFood-numCurrentFood) } - if foodSpawnChance > 0 && int(rand.Intn(100)) < foodSpawnChance { + if foodSpawnChance > 0 && GlobalRand.Intn(100) < foodSpawnChance { return false, PlaceFoodRandomly(GlobalRand, b, 1) } return false, nil diff --git a/standard_test.go b/standard_test.go index 188e176..f312323 100644 --- a/standard_test.go +++ b/standard_test.go @@ -3,7 +3,6 @@ package rules import ( "fmt" "math" - "math/rand" "testing" "github.com/stretchr/testify/require" @@ -278,7 +277,8 @@ func TestEatingOnLastMove(t *testing.T) { }, } - rand.Seed(0) // Seed with a value that will reliably not spawn food + // Seed with a value that will reliably not spawn food + SetGlobalSeed(0) r := getStandardRuleset(Settings{}) for _, test := range tests { _, nextState, err := r.Execute(test.prevState, test.moves) @@ -400,7 +400,7 @@ func TestHeadToHeadOnFood(t *testing.T) { }, } - rand.Seed(0) // Seed with a value that will reliably not spawn food + SetGlobalSeed(0) // Seed with a value that will reliably not spawn food r := getStandardRuleset(Settings{}) for _, test := range tests { _, nextState, err := r.Execute(test.prevState, test.moves) @@ -477,7 +477,8 @@ func TestRegressionIssue19(t *testing.T) { }, } - rand.Seed(0) // Seed with a value that will reliably not spawn food + // Seed with a value that will reliably not spawn food + SetGlobalSeed(0) r := getStandardRuleset(Settings{}) for _, test := range tests { _, nextState, err := r.Execute(test.prevState, test.moves) @@ -1560,7 +1561,7 @@ func TestMaybeSpawnFoodHalfChance(t *testing.T) { Food: test.Food, } - rand.Seed(test.Seed) + SetGlobalSeed(test.Seed) _, err := SpawnFoodStandard(b, r.Settings(), mockSnakeMoves()) require.NoError(t, err) require.Equal(t, test.ExpectedFood, len(b.Food), "Seed %d", test.Seed) diff --git a/test/test_utils.go b/test/test_utils.go index 06119ed..44fac6c 100644 --- a/test/test_utils.go +++ b/test/test_utils.go @@ -4,8 +4,8 @@ import ( "bytes" "encoding/json" "flag" - "io/ioutil" "log" + "os" "testing" "github.com/stretchr/testify/require" @@ -25,13 +25,13 @@ func RequireJSONMatchesFixture(t *testing.T, filename string, actual string) { var indented bytes.Buffer err := json.Indent(&indented, []byte(actual), "", " ") require.NoError(t, err, "Failed to indent JSON") - err = ioutil.WriteFile(filename, indented.Bytes(), 0644) + err = os.WriteFile(filename, indented.Bytes(), 0644) require.NoError(t, err, "Failed to update fixture", filename) log.Printf("Updating fixture file %#v", filename) } - expectedData, err := ioutil.ReadFile(filename) + expectedData, err := os.ReadFile(filename) require.NoError(t, err, "Failed to read fixture", filename) require.JSONEq(t, string(expectedData), actual)