package main import ( "fmt" "os" "time" "symph/audio" "symph/game" "symph/input" "symph/parameter" "symph/render" "github.com/lixenwraith/terminal" ) func main() { // Terminal init term := terminal.New() if err := term.Init(); err != nil { fmt.Fprintln(os.Stderr, "terminal init:", err) os.Exit(1) } defer term.Fini() // Game init state := game.New() // Audio init (silent fallback when backend unavailable) sfx := audio.NewMuter(audio.NewEngine()) defer sfx.Close() // Render init width, height := term.Size() renderer := render.NewRenderer(term, width, height, state) minW, minH := render.MinSize() // Ticker wakes the event loop for UI frames (~60fps) // The engine strictly owns beat quantization via Update(); frontend just pushes time forward ticker := time.NewTicker(parameter.GameRenderUpdate) defer ticker.Stop() go func() { for range ticker.C { term.PostEvent(terminal.Event{Type: terminal.EventKey, Key: terminal.KeyNone}) } }() renderer.Render(time.Now()) for { ev := term.PollEvent() switch ev.Type { case terminal.EventClosed, terminal.EventError: return case terminal.EventKey: // Quit on Escape, Ctrl+C, or 'q' if ev.Key == terminal.KeyEscape || ev.Key == terminal.KeyCtrlC || ev.Rune == 'q' || ev.Rune == 'Q' { return } // Toggle mute with Ctrl+S if ev.Key == terminal.KeyCtrlS { renderer.Muted = sfx.Toggle() } now := time.Now() // Viewport gate. Below the minimum the field is unrenderable; // freeze the simulation clock so no beats elapse behind the notice if w, h := term.Size(); w < minW || h < minH { state.Pause(now) renderer.Render(now) continue } state.Resume(now) // Real key: translate and dispatch with strict temporal timestamp if k := eventToKey(ev); k != input.KeyNone { state.Dispatch(input.Translate(k), now) } // Engine time step, event drain, render cycle every iteration // (Synthetic tick falls through to here naturally) state.Update(now) for _, ge := range state.DrainEvents() { sfx.Play(audio.MapGameEvent(ge)) } renderer.Render(now) case terminal.EventResize: renderer.Resize(ev.Width, ev.Height) now := time.Now() if ev.Width < minW || ev.Height < minH { state.Pause(now) } else { state.Resume(now) } renderer.Render(time.Now()) } } } // eventToKey translates a terminal key event to a platform-neutral input.Key func eventToKey(ev terminal.Event) input.Key { if ev.Key == terminal.KeyEnter { return input.KeyEnter } switch ev.Rune { case 'h', 'H': return input.KeyH case 'j', 'J': return input.KeyJ case 'k', 'K': return input.KeyK case 'l', 'L': return input.KeyL case ';': return input.KeySemicolon case 'a', 'A': return input.KeyA case 's', 'S': return input.KeyS case 'd', 'D': return input.KeyD case 'f', 'F': return input.KeyF case ' ': return input.KeySpace } return input.KeyNone }