clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

feat/app-decomposition
puzzletea / games / rippleeffect / gamemode.go
gamemode.go 3.8K
1package rippleeffect23import (4 _ "embed"5 "math/rand/v2"67 "github.com/FelineStateMachine/puzzletea/game"8 "github.com/FelineStateMachine/puzzletea/gameentry"9 "github.com/FelineStateMachine/puzzletea/puzzle"10)1112//go:embed help.md13var HelpContent string1415type Mode struct {16 game.BaseMode17 Size int18 MaxCage int19 GivenRatio float6420 profile generationProfile21}2223var (24 _ game.Mode = Mode{}25 _ game.Spawner = Mode{}26 _ game.SeededSpawner = Mode{}27)2829func NewMode(title, description string, size, maxCage int, givenRatio float64) Mode {30 return NewModeWithProfile(title, description, size, maxCage, givenRatio, defaultGenerationProfile(maxCage))31}3233func NewModeWithProfile(title, description string, size, maxCage int, givenRatio float64, profile generationProfile) Mode {34 return Mode{35 BaseMode: game.NewBaseMode(title, description),36 Size: size,37 MaxCage: maxCage,38 GivenRatio: givenRatio,39 profile: profile,40 }41}4243func (m Mode) Spawn() (game.Gamer, error) {44 puzzle, err := m.generatePuzzle()45 if err != nil {46 return nil, err47 }48 return New(m, puzzle)49}5051func (m Mode) SpawnSeeded(rng *rand.Rand) (game.Gamer, error) {52 puzzle, err := m.generatePuzzleSeeded(rng)53 if err != nil {54 return nil, err55 }56 return New(m, puzzle)57}5859var Modes = []game.Mode{60 NewModeWithProfile(61 "Mini 5x5",62 "Compact cages with extra anchors for quick local reads.",63 5,64 3,65 0.69,66 generationProfile{67 cageWeights: []int{0, 5, 6, 3},68 frontierSamples: 3,69 shapeBias: shapeBiasCompact,70 minGivensByCage: []int{0, 1, 1, 2},71 maxSingletonCages: -1,72 },73 ),74 NewModeWithProfile(75 "Easy 6x6",76 "Gentle spacing logic with compact cages and steady clues.",77 6,78 3,79 0.64,80 generationProfile{81 cageWeights: []int{0, 3, 5, 4},82 frontierSamples: 3,83 shapeBias: shapeBiasCompact,84 minGivensByCage: []int{0, 1, 1, 2},85 maxSingletonCages: -1,86 },87 ),88 NewModeWithProfile(89 "Medium 7x7",90 "Mixed cage shapes with sparser anchors and roomier deductions.",91 7,92 4,93 0.50,94 generationProfile{95 cageWeights: []int{0, 0, 3, 5, 4},96 frontierSamples: 2,97 shapeBias: shapeBiasNeutral,98 minGivensByCage: []int{0, 0, 0, 0, 0},99 maxSingletonCages: 1,100 },101 ),102 NewModeWithProfile(103 "Hard 8x8",104 "Longer cages and sparse anchors reward broader ripple reads.",105 8,106 4,107 0.46,108 generationProfile{109 cageWeights: []int{0, 0, 1, 4, 5},110 frontierSamples: 3,111 shapeBias: shapeBiasWinding,112 minGivensByCage: []int{0, 0, 0, 0, 0},113 maxSingletonCages: 1,114 },115 ),116 NewModeWithProfile(117 "Expert 9x9",118 "Winding large cages and sparse anchors push global deductions.",119 9,120 5,121 0.42,122 generationProfile{123 cageWeights: []int{0, 0, 1, 2, 4, 5},124 frontierSamples: 4,125 shapeBias: shapeBiasWinding,126 minGivensByCage: []int{0, 0, 0, 0, 0, 0},127 maxSingletonCages: 1,128 },129 ),130}131132var ModeDefinitions = gameentry.BuildModeDefs(Modes)133134var Definition = puzzle.NewDefinition(puzzle.DefinitionSpec{135 Name: "Ripple Effect",136 Description: "Fill the cages with sequential numbers without violating ripple distance.",137 Aliases: []string{"ripple"},138 Modes: ModeDefinitions,139 DailyModeIDs: puzzle.SelectModeIDsByIndex(ModeDefinitions, 1, 2, 3),140})141142var Entry = gameentry.NewEntry(gameentry.EntrySpec{143 Definition: Definition,144 Help: HelpContent,145 Import: game.AdaptImport(ImportModel),146 Modes: Modes,147 Print: PDFPrintAdapter,148})149150func (m Mode) generatePuzzle() (Puzzle, error) {151 rng := rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64()))152 return m.generatePuzzleSeeded(rng)153}154155func (m Mode) generatePuzzleSeeded(rng *rand.Rand) (Puzzle, error) {156 return generatePuzzleSeededWithProfile(m.Size, m.Size, m.MaxCage, m.GivenRatio, m.profile.withDefaults(m.MaxCage), rng)157}