package game import ( "fmt" "math/rand/v2" "strings" "symph/parameter" "symph/types" ) // patternDef is an authored gameplay segment. Patterns are hand-built to stay // fair: every chord leaves a reachable safe cell, hazards are telegraphed by // preceding pickups. minLevel gates entry into the selection pool type patternDef struct { minLevel int chords []string } var patternDefs = []patternDef{ // --- Level 1: pickup lines, no hazards --- {1, []string{ // center run "...|.*.|...", "...|.*.|...", "...|.*.|...", }}, {1, []string{ // left lane run "...|*..|...", "...|*..|...", "...|*..|...", }}, {1, []string{ // right lane run "...|..*|...", "...|..*|...", "...|..*|...", }}, {1, []string{ // jump arc "...|.*.|...", ".*.|...|...", ".*.|...|...", "...|.*.|...", }}, {1, []string{ // slide dip "...|.*.|...", "...|...|.*.", "...|...|.*.", "...|.*.|...", }}, {1, []string{ // lane sweep L→R "...|*..|...", "...|.*.|...", "...|..*|...", }}, // --- Level 2: single telegraphed obstacles --- {2, []string{ // hop wall: energy above hazard "...|.*.|...", ".*.|.#.|...", "...|.*.|...", }}, {2, []string{ // duck wall: energy below hazard "...|.*.|...", "...|.#.|.*.", "...|.*.|...", }}, {2, []string{ // side pinch: hold center "...|.*.|...", "...|#.#|...", "...|#.#|...", "...|.*.|...", }}, {2, []string{ // forced left lane "...|*..|...", "...|*.#|...", "...|*.#|...", "...|.*.|...", }}, {2, []string{ // magnet lure "...|.*.|...", "...|.M.|...", "*.*|...|*.*", "...|.*.|...", }}, {2, []string{ // shield cache: armor, then the wall that spends it "...|.*.|...", "...|.S.|...", "...|.#.|...", "...|.*.|...", }}, {2, []string{ // boost dash — tempo alone, no hazards "...|.B.|...", "...|.*.|...", "...|*..|...", "...|..*|...", "...|*..|...", "...|.*.|...", }}, // --- Level 3+: combinations --- {3, []string{ // hop then duck "...|.*.|...", ".*.|.#.|...", "...|.*.|...", "...|.#.|.*.", "...|.*.|...", }}, {3, []string{ // tunnel: mid row only (Down cancels a stray jump in time) "...|.*.|...", "###|.S.|###", "###|.*.|###", "###|.*.|###", "...|.*.|...", }}, {3, []string{ // top weave R→L over walls (single jump spans it at base PDZ) "...|.*.|...", "..*|.##|...", ".*.|#.#|...", "*..|##.|...", "...|.*.|...", }}, {3, []string{ // pillar dodge: center blocked, side lanes rewarded "...|.*.|...", ".#.|*#*|.#.", "...|.*.|...", }}, {3, []string{ // trap lane: center reopens for a single chord, then shuts again. Exercises the TimerTrapSuffix marker "...|.*.|...", "...|.#.|...", "...|.#.|...", ".*.|...|...", "...|.#.|...", "...|.#.|...", "...|.*.|...", }}, {3, []string{ // boost run: hazard-free corridor — the double tempo turns the lane hops into the skill test "...|.B.|...", "...|.*.|...", "...|*.*|...", "...|.*.|...", "...|*.*|...", "...|.*.|...", }}, {3, []string{ // boost gauntlet — mid-center is open at every chord "...|.B.|...", "...|.*.|...", "#.#|.*.|#.#", "*.*|.*.|*.*", "#.#|.*.|#.#", "...|.*.|...", }}, {4, []string{ // armored gauntlet: a walking wall run. Weave the mid row or jump it; the shield covers one mistimed lane change "...|.S.|...", "...|.*.|...", "...|##.|...", "...|.##|...", "...|#.#|...", "...|.*.|...", }}, } // pattern is the parsed, playable form type pattern struct { minLevel int chords []types.Chord } var patterns []pattern func init() { patterns = make([]pattern, len(patternDefs)) for i, d := range patternDefs { p := pattern{minLevel: d.minLevel, chords: make([]types.Chord, len(d.chords))} for j, s := range d.chords { p.chords[j] = parseChord(s) } patterns[i] = p } } // parseChord decodes a "TTT|MMM|BBB" literal; panics on malformed input or on // a kind the engine cannot resolve (author error, caught at process start). // Byte indexing is sound: types asserts every glyph is ASCII func parseChord(s string) types.Chord { rows := strings.Split(s, "|") if len(rows) != parameter.GamePlayIndexYMax { panic(fmt.Sprintf("pattern chord %q: want %d rows", s, parameter.GamePlayIndexYMax)) } var c types.Chord for y, row := range rows { if len(row) != parameter.GamePlayIndexXMax { panic(fmt.Sprintf("pattern row %q: want %d cells", row, parameter.GamePlayIndexXMax)) } for x, r := range row { v, ok := types.ValueByGlyph(r) if !ok { panic(fmt.Sprintf("pattern cell %q: unknown glyph", string(r))) } if v != types.ValueNone && resolvers[v] == nil { panic(fmt.Sprintf("pattern cell %q: %s has no engine resolver", string(r), v)) } c.Notes[y][x] = types.Note{Value: v} } } return c } // newSong builds a level: intro rest, pattern chains separated by rest gaps // that shrink with level, outro rest. The pool widens as level unlocks higher // minLevel entries; chains lengthen with level func newSong(level int) *types.Song { length := min( parameter.SongBaseLength+(level-1)*parameter.SongLengthPerLevel, parameter.GamePlayIndexZMax, ) pool := make([]pattern, 0, len(patterns)) for _, p := range patterns { if p.minLevel <= level { pool = append(pool, p) } } chords := make([]types.Chord, 0, length) chords = append(chords, make([]types.Chord, parameter.GenIntroRest)...) budget := length - parameter.GenOutroRest rest := max(parameter.GenRestBase-(level-1), parameter.GenRestMin) build: for { chain := parameter.GenChainBase + rand.IntN(min(level, parameter.GenChainMax-parameter.GenChainBase)+1) for range chain { p := pool[rand.IntN(len(pool))] if len(chords)+len(p.chords) > budget { break build } // Value-copy append: runtime consumption mutates the Song, // never the templates chords = append(chords, p.chords...) if rand.Float64() < parameter.GenBreatherProbability && len(chords) < budget { chords = append(chords, types.Chord{}) } } if len(chords)+rest > budget { break } chords = append(chords, make([]types.Chord, rest)...) } // Pad to exact length; tail padding doubles as outro rest for len(chords) < length { chords = append(chords, types.Chord{}) } return &types.Song{Chords: chords} }