Skip to content

Commit d260f2e

Browse files
tweaked nurikabe to be more reasonably difficult/generation speed trade of
1 parent 0473445 commit d260f2e

2 files changed

Lines changed: 50 additions & 7 deletions

File tree

nurikabe/generator.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
const (
1515
generationHardTimeout = 10 * time.Second
1616
uniquenessMinRemainingBudget = 250 * time.Millisecond
17+
frontierBiasSampleCount = 7
1718
)
1819

1920
type generationBudget struct {
@@ -677,12 +678,41 @@ func (s *candidateState) popFrontier(rng *rand.Rand) (point, bool) {
677678
if len(s.frontier) == 0 {
678679
return point{}, false
679680
}
680-
idx := rng.IntN(len(s.frontier))
681-
picked := s.frontier[idx]
681+
682+
bestIdx := -1
683+
bestScore := -1
684+
samples := min(frontierBiasSampleCount, len(s.frontier))
685+
for range samples {
686+
idx := rng.IntN(len(s.frontier))
687+
score := s.frontierPriorityScore(s.frontier[idx])
688+
if score > bestScore || (score == bestScore && rng.IntN(2) == 0) {
689+
bestScore = score
690+
bestIdx = idx
691+
}
692+
}
693+
694+
if bestIdx < 0 {
695+
bestIdx = rng.IntN(len(s.frontier))
696+
}
697+
698+
picked := s.frontier[bestIdx]
682699
s.removeFrontier(picked)
683700
return picked, true
684701
}
685702

703+
func (s *candidateState) frontierPriorityScore(p point) int {
704+
if !s.inBounds(p) || s.sea[p.y][p.x] {
705+
return 0
706+
}
707+
708+
label := s.labels[p.y][p.x]
709+
componentSize := s.componentSize[label]
710+
sameLabelNeighbors := len(s.landNeighborsWithLabel(p, label))
711+
712+
// Favor carving from larger components; local degree nudges toward useful splits.
713+
return componentSize*16 + sameLabelNeighbors*3
714+
}
715+
686716
func (s *candidateState) removeComponent(label int) {
687717
size, ok := s.componentSize[label]
688718
if !ok {
@@ -1048,7 +1078,7 @@ func modeIslandProfile(mode NurikabeMode) islandProfile {
10481078
minAverageSize: 1.8,
10491079
maxSingletonRatio: 0.72,
10501080
minLargestIsland: 4,
1051-
maxLargestIsland: 20,
1081+
maxLargestIsland: 15,
10521082
}
10531083
case "Expert":
10541084
return islandProfile{
@@ -1059,7 +1089,7 @@ func modeIslandProfile(mode NurikabeMode) islandProfile {
10591089
minAverageSize: 1.7,
10601090
maxSingletonRatio: 0.78,
10611091
minLargestIsland: 4,
1062-
maxLargestIsland: 22,
1092+
maxLargestIsland: 17,
10631093
}
10641094
default:
10651095
return islandProfile{

nurikabe/nurikabe_test.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -590,10 +590,23 @@ func TestConstructedCandidateValidCompletedBoard(t *testing.T) {
590590
clueTarget = 2
591591
}
592592

593-
rng := rand.New(rand.NewPCG(901, 1777))
594-
candidate, err := buildCandidateByCarving(mode, clueTarget, profile, rng, 0)
593+
var (
594+
candidate candidateResult
595+
err error
596+
)
597+
598+
// Keep this deterministic while avoiding reliance on a single RNG path.
599+
for attempt := range 24 {
600+
seedA := uint64(901 + attempt*17)
601+
seedB := uint64(1777 + attempt*29)
602+
rng := rand.New(rand.NewPCG(seedA, seedB))
603+
candidate, err = buildCandidateByCarving(mode, clueTarget, profile, rng, attempt)
604+
if err == nil {
605+
break
606+
}
607+
}
595608
if err != nil {
596-
t.Fatalf("buildCandidateByCarving error: %v", err)
609+
t.Fatalf("buildCandidateByCarving failed across deterministic attempts: %v", err)
597610
}
598611

599612
if err := validateClues(candidate.puzzle.Clues, candidate.puzzle.Width, candidate.puzzle.Height); err != nil {

0 commit comments

Comments
 (0)