app

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package app is the interactive shell: the screens a player moves between and the state they share. The view layer in internal/ui draws; this package decides what is drawn and what the keys do.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Back

func Back() tea.Cmd

Back leaves the current screen and returns to the previous one.

func Done

func Done(msg DoneMsg) tea.Cmd

Done is a command that emits a DoneMsg, which is what a screen returns from Update when it is finished.

func Fail

func Fail(err error) tea.Cmd

Fail leaves the current screen and reports why.

func OnboardingSeen added in v0.2.0

func OnboardingSeen(d Deps, player string) bool

OnboardingSeen reports whether the player at this keyboard has been through the introduction.

It answers for the profile that is playing, because being introduced to the game is something that happened to a person and not to a machine: two people sharing a machine with a profile each are two players, and the second of them is exactly the newcomer the introduction was written for. internal/profile holds the flag and the argument for it.

Where there is no profile to ask about — no store, or nobody chosen yet — it answers false, so the introduction is offered rather than silently skipped. That is the safer of the two wrong answers: offering it again costs one keypress, and never offering it loses the feature to whoever needed it most. In practice the question does not arise, because the launch path asks who is playing before it shows anything a player can reach this from.

func Open

func Open(s Screen) tea.Cmd

Open shows another screen without giving up this one.

This is the difference between a menu that launches a game and a menu that is consumed by launching one. With Replace, leaving the game emptied the stack and ended the program, so there was no way back to the menu the player had started from.

func Quit

func Quit() tea.Cmd

Quit ends the program from anywhere.

func Replace

func Replace(s Screen) tea.Cmd

Replace swaps the current screen for another. The screen asking is finished and will not be returned to.

Types

type Departing

type Departing interface {
	// Depart is called once, on the way out, before the program ends. It must
	// not block for long and must not expect to draw again.
	Depart()
}

Departing is implemented by a screen that has something to finish before the program ends, such as saving a game that is still in progress.

The shell answers the global quit key itself so that a busy screen cannot trap the player, but that means the key never reaches the screen. Without this, a screen's own handling of it is dead code: quitting with the plain letter saved an unfinished game while quitting with the control key silently discarded it, which is the same act from the player's point of view.

type Deps

type Deps struct {
	// ConfigDir is where profiles, games and settings live.
	ConfigDir string

	Profiles *profile.Store
	Board    *leaderboard.Board
	Games    *gamestore.Store

	Theme  theme.Theme
	Styles *ui.Styles
	Keymap ui.Keymap

	// Now is the clock, injected so that a test can pin the durations that end
	// up on the leaderboard.
	Now func() time.Time

	// Note lets a screen leave a line for the player to read after the
	// interface has closed. Anything printed while the alternate screen is up
	// disappears with it, so a screen that saved a game on the way out has
	// nowhere to say so: the player was left not knowing whether their game
	// survived. Nil means nobody is listening.
	Note func(string)
}

Deps are the collaborators every screen needs. It is passed by value; the stores inside it are shared and safe for concurrent use.

func (Deps) Clock

func (d Deps) Clock() time.Time

Clock returns the time, defaulting to the real one.

type DoneMsg

type DoneMsg struct {
	Next Screen
	Err  error
	Quit bool
}

DoneMsg asks the shell to leave the screen that sent it.

A screen that wants to be replaced sets Next. A screen that wants to go back to where it came from leaves Next nil. A screen that failed sets Err, which the shell shows before going back. Quit ends the program.

type GameConfig

type GameConfig struct {
	Kind  gamestore.Kind
	Rules game.Ruleset

	// Seats says who plays each side. Both sides must be filled.
	Seats map[game.Player]Seat

	// Hints allows the player to ask for advice, which only makes sense when
	// there is an engine to ask.
	Hints bool
	// HintFor is the engine consulted for hints, which is the opponent bot in a
	// game against one and may be a separate engine otherwise.
	HintFor bot.Bot

	// Session is the network connection for a live remote game, nil otherwise.
	Session netplay.Session

	// Codes drives a remote seat by move codes the players exchange by hand,
	// with no connection at all. It is mutually exclusive with Session: a
	// correspondence game has a remote seat and no session, which is the one
	// case where that combination is legitimate.
	Codes bool

	// Resume, when set, continues a stored game instead of starting a new one.
	Resume *gamestore.Saved
	// StoreID is the identifier the game is saved under, and for a
	// correspondence game it is also the identifier its move codes are bound
	// to, so a code from another game is refused. Empty means allocate one.
	StoreID string
}

GameConfig describes a game to be played. The command line builds one of these and hands it to NewGameScreen.

func (GameConfig) LocalSide

func (c GameConfig) LocalSide() (game.Player, bool)

LocalSide returns the side the player at this keyboard is on, and whether exactly one side is local. In a hotseat game both sides are local, so this reports false.

func (GameConfig) Opponent

func (c GameConfig) Opponent(local game.Player) string

Opponent returns the leaderboard name for the side opposing the local player.

type Menu struct {
	// contains filtered or unexported fields
}

Menu is the main menu: everything a player can reach, each entry with a one-line explanation shown while it is highlighted, plus the small forms that collect the choices a game needs before it can start.

The forms live inside this model rather than being screens of their own because a screen cannot push another screen: the shell's DoneMsg replaces or pops, so a wizard built out of screens could not walk backwards.

func NewMenu

func NewMenu(d Deps, player string) *Menu

NewMenu returns the main menu for a player.

func (m *Menu) Init() tea.Cmd

Init implements tea.Model. On the first run — of this profile, on this machine — the introduction opens on top of the menu, so that finishing or skipping it lands the player here, where everything else is. The check belongs in Init and not in revealed(): Init runs once, when the menu is built, so a player coming back from a game cannot be handed the tour a second time.

func (m *Menu) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update implements tea.Model.

func (m *Menu) View() tea.View

View implements tea.Model. The shell owns the alternate screen, so only the content is set.

type Noting added in v0.1.1

type Noting interface {
	// DepartNote returns the line and forgets it, so that one departure is
	// announced exactly once.
	DepartNote() string
}

Noting is implemented by a screen that has a line for the player on its way out, such as which game it has just saved and how to open it again.

The screen does not print the line itself because it cannot know where the player will read it. A departure that ends the program has to leave it for the command line, since the interface takes its own output with it; a departure that goes back to the menu has a screen to say it on, and saying it there, at the time, is the whole point of saying it. The shell is what knows the difference, so it collects the line and places it.

type OpenMsg

type OpenMsg struct {
	Screen Screen
}

OpenMsg asks the shell to show another screen on top of the one that sent it, which stays underneath and is returned to when the new one is finished.

type Picker

type Picker struct {
	// contains filtered or unexported fields
}

Picker asks who is playing, which is the first thing the program does (R13). It satisfies R14 two ways at once: the list is browsable with an empty query, most recently played first, for someone who cannot recall how they spelled their name, and typing filters it fuzzily for someone who can nearly recall it. A name that is not in the list is offered for creation, which is also the whole of the first-run path (R12).

func NewPicker

func NewPicker(d Deps, prompt string) *Picker

NewPicker returns the profile chooser. The prompt is the question at the top of the screen; an empty one falls back to the launch wording.

Once a name is chosen the picker replaces itself with the main menu, which is what launching the program does. Chosen overrides that for a caller that wants the name for something else.

func (*Picker) Cancelled

func (p *Picker) Cancelled(f func() tea.Cmd) *Picker

Cancelled sets what escape does. The picker has no answer of its own: at launch it is the first screen, and a program that ends on one stray escape is worse than one that ignores the key. Setting this is therefore what puts "esc back" on the status line as well, so the offer and the key arrive together.

func (*Picker) Chosen

func (p *Picker) Chosen(f func(name string) tea.Cmd) *Picker

Chosen replaces what happens once a name has been picked. It is how the picker is embedded in another screen: the command can emit a message that screen handles instead of leaving the picker.

The default builds a fresh menu and replaces the picker with it, which is right when the picker is the program's first screen and there is nothing behind it. A caller that opened the picker on top of its own screen must set this, or the replacement leaves a second menu stacked on the first and the screen the player came from is two deep and never returned to. The choice is persisted before this runs, so a handler only has to decide where the player goes next.

func (*Picker) Init

func (p *Picker) Init() tea.Cmd

Init implements tea.Model.

func (*Picker) Update

func (p *Picker) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update implements tea.Model.

func (*Picker) View

func (p *Picker) View() tea.View

View implements tea.Model. The shell owns the alternate screen, so only the content is set here.

type ReplayScreen

type ReplayScreen struct {
	// contains filtered or unexported fields
}

ReplayScreen walks through a finished game one move at a time.

Positions are materialised up front by replaying the record, rather than by undoing from the end. Replaying forwards is the same path a saved game takes when it is loaded, so a record that cannot be replayed is refused here too instead of appearing to work until the player steps back through it.

func (*ReplayScreen) Init

func (s *ReplayScreen) Init() tea.Cmd

Init satisfies tea.Model.

func (*ReplayScreen) Update

func (s *ReplayScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update satisfies tea.Model.

func (*ReplayScreen) View

func (s *ReplayScreen) View() tea.View

View satisfies tea.Model.

type Screen

type Screen interface {
	tea.Model
}

Screen is one place the player can be. Screens are Bubble Tea models; they hand control back by emitting a DoneMsg rather than by returning tea.Quit, so that the shell decides what happens next.

func NewGameScreen

func NewGameScreen(d Deps, cfg GameConfig) (Screen, error)

NewGameScreen builds the screen for one game. Both sides must have a seat and at least one of them must be played at this keyboard. A remote seat is driven either by a live session or, in a correspondence game, by the move codes the players exchange by hand; it needs exactly one of the two.

func NewOnboarding added in v0.2.0

func NewOnboarding(d Deps, player string) (Screen, error)

NewOnboarding builds the introduction. It is a Screen like any other: the caller decides whether to show it, and the shell decides where leaving it goes. player is the profile the run is playing as. It is passed in rather than read from the store because the two can differ for the length of one command.

func NewReplayScreen

func NewReplayScreen(d Deps, saved gamestore.Saved) (Screen, error)

NewReplayScreen prepares a stored game for review.

func NewTutorialScreen

func NewTutorialScreen(d Deps, lessonID string) (Screen, error)

NewTutorialScreen opens the interactive tutorial. An empty lessonID starts at the lesson chooser; a lesson id starts that lesson, and leaving it then leaves the screen rather than dropping into a chooser the player never saw.

type Seat

type Seat struct {
	// Profile is the local player's name, empty for a bot or a remote opponent.
	Profile string
	// Bot is the opponent engine, nil unless this seat is a bot.
	Bot bot.Bot
	// Remote marks a seat driven by a networked opponent.
	Remote bool
	// Label is what the interface calls this seat.
	Label string
}

Seat says who is playing one side of a game.

func (Seat) Human

func (s Seat) Human() bool

Human reports whether this seat is played at this keyboard.

type Shell

type Shell struct {
	// contains filtered or unexported fields
}

Shell is the root Bubble Tea model: it owns the stack of screens, the terminal size, the alternate screen and the error banner, and it is the only thing in the program that calls tea.Quit.

Screens hand control back with a DoneMsg instead of quitting or pushing themselves, so the rule for what happens when a screen finishes lives in one place and a screen can be reached from more than one place without knowing where it will return to.

func NewShell

func NewShell(d Deps, first Screen) *Shell

NewShell returns the root model showing first.

func (*Shell) Init

func (s *Shell) Init() tea.Cmd

Init implements tea.Model.

func (*Shell) Push

func (s *Shell) Push(sc Screen) tea.Cmd

Push shows sc on top of the current screen, which stays on the stack and is revealed again when sc finishes.

func (*Shell) Update

func (s *Shell) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update implements tea.Model. Everything except the shell's own messages and the global quit key goes to the screen on top.

func (*Shell) View

func (s *Shell) View() tea.View

View implements tea.Model. The shell owns the alternate screen, so no screen has to; only a screen's content is used.

type ThemeChangedMsg

type ThemeChangedMsg struct {
	Theme  theme.Theme
	Styles *ui.Styles
}

ThemeChangedMsg announces that the player chose a different colour scheme. The shell follows it for its own drawing and passes it on, so a screen holding its own copy of Deps can follow too.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL