1886 lines
55 KiB
Go
1886 lines
55 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
// GameEntry — запись в реестре игр коллекции. Регистр сделан так,
|
||
// чтобы добавление новой игры сводилось к добавлению одной записи
|
||
// в gameRegistry, без изменений в логике меню.
|
||
type GameEntry struct {
|
||
Name func() string
|
||
Description func() string
|
||
Rules func() string
|
||
New func() tea.Model
|
||
|
||
// UsesStakes — включает экран настройки ставки перед запуском:
|
||
// стартовый капитал, размер анте и множитель банка при особой
|
||
// победе (в Тонке — объявление Тонка). Если false, игра
|
||
// запускается сразу через New(), без экрана настроек.
|
||
UsesStakes bool
|
||
NewWithStakes func(startingCapital, ante, bonusMultiplier int) tea.Model
|
||
|
||
// UsesPlayerCount — включает экран выбора количества игроков
|
||
// (2-6, включая человека) перед запуском.
|
||
UsesPlayerCount bool
|
||
NewWithPlayerCount func(numPlayers int) tea.Model
|
||
|
||
// UsesBlackjackSettings — включает совмещённый экран настройки
|
||
// перед запуском: число игроков, стартовый капитал и размер
|
||
// ставки за раунд. Отдельный флаг вместо переиспользования
|
||
// UsesStakes/UsesPlayerCount, потому что Блэкджеку нужны все три
|
||
// параметра сразу на одном экране.
|
||
UsesBlackjackSettings bool
|
||
NewBlackjackWithSettings func(numPlayers, startingCapital, bet int) tea.Model
|
||
|
||
// UsesCheckersSettings — включает экран выбора уровня сложности
|
||
// бота (простой/сильный) перед началом партии.
|
||
UsesCheckersSettings bool
|
||
NewCheckersWithDifficulty func(depth int) tea.Model
|
||
|
||
// UsesTicTacToeSettings — включает экран выбора уровня сложности
|
||
// бота (лёгкий/средний/непобедимый) перед партией.
|
||
UsesTicTacToeSettings bool
|
||
NewTicTacToeWithDifficulty func(difficulty TicTacToeDifficulty) tea.Model
|
||
|
||
// UsesNardySettings — включает экран выбора варианта правил
|
||
// (короткие/длинные) и уровня сложности бота перед партией.
|
||
UsesNardySettings bool
|
||
NewNardyWithSettings func(variant NardyVariant, difficulty NardyDifficulty) tea.Model
|
||
|
||
// UsesUrSettings — включает экран выбора уровня сложности бота
|
||
// перед партией в Ур.
|
||
UsesUrSettings bool
|
||
NewUrWithDifficulty func(difficulty UrDifficulty) tea.Model
|
||
|
||
// UsesSenetSettings — включает экран выбора уровня сложности
|
||
// бота перед партией в Сенет.
|
||
UsesSenetSettings bool
|
||
NewSenetWithDifficulty func(difficulty SenetDifficulty) tea.Model
|
||
|
||
// UsesChessSettings — то же самое, но для шахмат (свой уровень
|
||
// глубины поиска, т.к. ветвление в шахматах намного шире).
|
||
UsesChessSettings bool
|
||
NewChessWithDifficulty func(depth int) tea.Model
|
||
|
||
// UsesGoSettings — экран с двумя полями: размер доски (9/13/19)
|
||
// и уровень сложности бота.
|
||
UsesGoSettings bool
|
||
NewGoWithSettings func(size int, difficulty GoDifficulty) tea.Model
|
||
|
||
// UsesMinesweeperSettings — экран выбора пресета размера поля
|
||
// (Новичок/Любитель/Эксперт).
|
||
UsesMinesweeperSettings bool
|
||
NewMinesweeperWithPreset func(width, height, mines int) tea.Model
|
||
}
|
||
|
||
// gameRegistry — список доступных игр коллекции.
|
||
var gameRegistry = []GameEntry{
|
||
{
|
||
Name: func() string { return T("game.tonk.name") },
|
||
Description: func() string { return T("game.tonk.desc") },
|
||
Rules: tonkRules,
|
||
New: func() tea.Model { return NewModel() },
|
||
UsesStakes: true,
|
||
NewWithStakes: func(capital, ante, mult int) tea.Model { return NewModelWithStakes(capital, ante, mult) },
|
||
},
|
||
{
|
||
Name: func() string { return T("game.durak.name") },
|
||
Description: func() string { return T("game.durak.desc") },
|
||
Rules: durakRules,
|
||
New: func() tea.Model { return NewDurakModel(4) },
|
||
UsesPlayerCount: true,
|
||
NewWithPlayerCount: func(n int) tea.Model { return NewDurakModel(n) },
|
||
},
|
||
{
|
||
Name: func() string { return T("game.bj.name") },
|
||
Description: func() string { return T("game.bj.desc") },
|
||
Rules: blackjackRules,
|
||
UsesBlackjackSettings: true,
|
||
NewBlackjackWithSettings: func(numPlayers, capital, bet int) tea.Model {
|
||
return NewBlackjackModel(numPlayers, capital, bet)
|
||
},
|
||
},
|
||
{
|
||
Name: func() string { return T("game.101.name") },
|
||
Description: func() string { return T("game.101.desc") },
|
||
Rules: oneOhOneRules,
|
||
New: func() tea.Model { return NewOneOhOneModel(4) },
|
||
UsesPlayerCount: true,
|
||
NewWithPlayerCount: func(n int) tea.Model { return NewOneOhOneModel(n) },
|
||
},
|
||
{
|
||
Name: func() string { return T("game.1000.name") },
|
||
Description: func() string { return T("game.1000.desc") },
|
||
Rules: thousandRules,
|
||
New: func() tea.Model { return NewThousandModel() },
|
||
},
|
||
{
|
||
Name: func() string { return T("game.klondike.name") },
|
||
Description: func() string { return T("game.klondike.desc") },
|
||
Rules: klondikeRules,
|
||
New: func() tea.Model { return NewKlondikeModel() },
|
||
},
|
||
{
|
||
Name: func() string { return T("game.checkers.name") },
|
||
Description: func() string { return T("game.checkers.desc") },
|
||
Rules: checkersRules,
|
||
UsesCheckersSettings: true,
|
||
NewCheckersWithDifficulty: func(depth int) tea.Model {
|
||
return NewCheckersModel(depth)
|
||
},
|
||
},
|
||
{
|
||
Name: func() string { return T("game.chess.name") },
|
||
Description: func() string { return T("game.chess.desc") },
|
||
Rules: chessRules,
|
||
UsesChessSettings: true,
|
||
NewChessWithDifficulty: func(depth int) tea.Model {
|
||
return NewChessModel(depth)
|
||
},
|
||
},
|
||
{
|
||
Name: func() string { return T("game.corpsestarch.name") },
|
||
Description: func() string { return T("game.corpsestarch.desc") },
|
||
Rules: corpseStarchRules,
|
||
New: func() tea.Model { return NewCorpseStarchModel() },
|
||
},
|
||
{
|
||
Name: func() string { return T("game.go.name") },
|
||
Description: func() string { return T("game.go.desc") },
|
||
Rules: goRules,
|
||
UsesGoSettings: true,
|
||
NewGoWithSettings: func(size int, difficulty GoDifficulty) tea.Model {
|
||
return NewGoModel(size, difficulty)
|
||
},
|
||
},
|
||
{
|
||
Name: func() string { return T("game.minesweeper.name") },
|
||
Description: func() string { return T("game.minesweeper.desc") },
|
||
Rules: minesweeperRules,
|
||
UsesMinesweeperSettings: true,
|
||
NewMinesweeperWithPreset: func(width, height, mines int) tea.Model {
|
||
return NewMinesweeperModel(width, height, mines)
|
||
},
|
||
},
|
||
{
|
||
Name: func() string { return T("game.wayfarer.name") },
|
||
Description: func() string { return T("game.wayfarer.desc") },
|
||
Rules: wayfarerRules,
|
||
New: func() tea.Model { return NewWayfarerModel() },
|
||
},
|
||
{
|
||
Name: func() string { return T("game.tictactoe.name") },
|
||
Description: func() string { return T("game.tictactoe.desc") },
|
||
Rules: ticTacToeRules,
|
||
UsesTicTacToeSettings: true,
|
||
NewTicTacToeWithDifficulty: func(difficulty TicTacToeDifficulty) tea.Model {
|
||
return NewTicTacToeModel(difficulty)
|
||
},
|
||
},
|
||
{
|
||
Name: func() string { return T("game.nardy.name") },
|
||
Description: func() string { return T("game.nardy.desc") },
|
||
Rules: nardyRules,
|
||
UsesNardySettings: true,
|
||
NewNardyWithSettings: func(variant NardyVariant, difficulty NardyDifficulty) tea.Model {
|
||
return NewNardyModel(variant, difficulty)
|
||
},
|
||
},
|
||
{
|
||
Name: func() string { return T("game.ur.name") },
|
||
Description: func() string { return T("game.ur.desc") },
|
||
Rules: urRules,
|
||
UsesUrSettings: true,
|
||
NewUrWithDifficulty: func(difficulty UrDifficulty) tea.Model {
|
||
return NewUrModel(difficulty)
|
||
},
|
||
},
|
||
{
|
||
Name: func() string { return T("game.senet.name") },
|
||
Description: func() string { return T("game.senet.desc") },
|
||
Rules: senetRules,
|
||
UsesSenetSettings: true,
|
||
NewSenetWithDifficulty: func(difficulty SenetDifficulty) tea.Model {
|
||
return NewSenetModel(difficulty)
|
||
},
|
||
},
|
||
}
|
||
|
||
type appState int
|
||
|
||
const (
|
||
stateLanguage appState = iota
|
||
stateMenu
|
||
stateRulesMenu
|
||
stateRules
|
||
stateSettings
|
||
statePlayerCount
|
||
stateBlackjackSettings
|
||
stateCheckersSettings
|
||
stateTicTacToeSettings
|
||
stateNardySettings
|
||
stateUrSettings
|
||
stateSenetSettings
|
||
stateChessSettings
|
||
stateGoSettings
|
||
stateMinesweeperSettings
|
||
statePlaying
|
||
)
|
||
|
||
// stakeSettings — экран настройки ставки перед игрой: значения
|
||
// перебираются по пресетам (а не вводятся текстом), чтобы обойтись
|
||
// без отдельного text-input компонента в TUI.
|
||
type stakeSettings struct {
|
||
capitalOptions []int
|
||
anteOptions []int
|
||
multiplierOptions []int
|
||
|
||
capitalIdx int
|
||
anteIdx int
|
||
multiplierIdx int
|
||
|
||
fieldCursor int // 0 = капитал, 1 = анте, 2 = множитель
|
||
gameIdx int // на какую игру из gameRegistry применится
|
||
}
|
||
|
||
func newStakeSettings(gameIdx int) stakeSettings {
|
||
return stakeSettings{
|
||
capitalOptions: []int{50, 100, 200, 500, 1000},
|
||
anteOptions: []int{1, 5, 10, 20, 50},
|
||
multiplierOptions: []int{2, 3, 4},
|
||
capitalIdx: 1, // 100
|
||
anteIdx: 1, // 5
|
||
multiplierIdx: 0, // 2
|
||
gameIdx: gameIdx,
|
||
}
|
||
}
|
||
|
||
func (s stakeSettings) capital() int { return s.capitalOptions[s.capitalIdx] }
|
||
func (s stakeSettings) ante() int { return s.anteOptions[s.anteIdx] }
|
||
func (s stakeSettings) multiplier() int { return s.multiplierOptions[s.multiplierIdx] }
|
||
|
||
// cycle сдвигает значение текущего выбранного поля на delta шагов
|
||
// (±1), с ограничением по краям списка пресетов (без зацикливания).
|
||
func (s *stakeSettings) cycle(delta int) {
|
||
clamp := func(idx, max int) int {
|
||
idx += delta
|
||
if idx < 0 {
|
||
return 0
|
||
}
|
||
if idx > max {
|
||
return max
|
||
}
|
||
return idx
|
||
}
|
||
switch s.fieldCursor {
|
||
case 0:
|
||
s.capitalIdx = clamp(s.capitalIdx, len(s.capitalOptions)-1)
|
||
case 1:
|
||
s.anteIdx = clamp(s.anteIdx, len(s.anteOptions)-1)
|
||
case 2:
|
||
s.multiplierIdx = clamp(s.multiplierIdx, len(s.multiplierOptions)-1)
|
||
}
|
||
}
|
||
|
||
// playerCountSettings — экран выбора количества игроков (2-6,
|
||
// включая человека) перед игрой вроде Дурака.
|
||
type playerCountSettings struct {
|
||
count int
|
||
gameIdx int
|
||
}
|
||
|
||
func newPlayerCountSettings(gameIdx int) playerCountSettings {
|
||
return playerCountSettings{count: 4, gameIdx: gameIdx}
|
||
}
|
||
|
||
func (s *playerCountSettings) cycle(delta int) {
|
||
s.count += delta
|
||
if s.count < 2 {
|
||
s.count = 2
|
||
}
|
||
if s.count > 6 {
|
||
s.count = 6
|
||
}
|
||
}
|
||
|
||
// blackjackSettings — совмещённый экран настройки перед Блэкджеком:
|
||
// число игроков (1-6, включая человека — можно играть и в одиночку
|
||
// против дилера), стартовый капитал и размер ставки за раунд.
|
||
// checkersSettings — экран выбора уровня сложности бота перед
|
||
// партией в шашки: два уровня, разница только в глубине поиска.
|
||
type checkersSettings struct {
|
||
levelIdx int
|
||
gameIdx int
|
||
}
|
||
|
||
var checkersLevels = []struct {
|
||
LabelKey string
|
||
Depth int
|
||
DescKey string
|
||
}{
|
||
{LabelKey: "checkerslvl.easy", Depth: CheckersDifficultyEasy, DescKey: "checkerslvl.easy_desc"},
|
||
{LabelKey: "checkerslvl.hard", Depth: CheckersDifficultyHard, DescKey: "checkerslvl.hard_desc"},
|
||
}
|
||
|
||
func newCheckersSettings(gameIdx int) checkersSettings {
|
||
return checkersSettings{levelIdx: 0, gameIdx: gameIdx}
|
||
}
|
||
|
||
func (s *checkersSettings) cycle(delta int) {
|
||
s.levelIdx += delta
|
||
if s.levelIdx < 0 {
|
||
s.levelIdx = 0
|
||
}
|
||
if s.levelIdx > len(checkersLevels)-1 {
|
||
s.levelIdx = len(checkersLevels) - 1
|
||
}
|
||
}
|
||
|
||
// ticTacToeSettings — экран выбора уровня сложности бота перед
|
||
// партией в крестики-нолики: три уровня (лёгкий/средний/непобедимый).
|
||
type ticTacToeSettings struct {
|
||
levelIdx int
|
||
gameIdx int
|
||
}
|
||
|
||
var ticTacToeLevels = []struct {
|
||
LabelKey string
|
||
Difficulty TicTacToeDifficulty
|
||
DescKey string
|
||
}{
|
||
{LabelKey: "tictactoelvl.easy", Difficulty: TicTacToeDifficultyEasy, DescKey: "tictactoelvl.easy_desc"},
|
||
{LabelKey: "tictactoelvl.medium", Difficulty: TicTacToeDifficultyMedium, DescKey: "tictactoelvl.medium_desc"},
|
||
{LabelKey: "tictactoelvl.hard", Difficulty: TicTacToeDifficultyHard, DescKey: "tictactoelvl.hard_desc"},
|
||
}
|
||
|
||
func newTicTacToeSettings(gameIdx int) ticTacToeSettings {
|
||
return ticTacToeSettings{levelIdx: 0, gameIdx: gameIdx}
|
||
}
|
||
|
||
func (s *ticTacToeSettings) cycle(delta int) {
|
||
s.levelIdx += delta
|
||
if s.levelIdx < 0 {
|
||
s.levelIdx = 0
|
||
}
|
||
if s.levelIdx > len(ticTacToeLevels)-1 {
|
||
s.levelIdx = len(ticTacToeLevels) - 1
|
||
}
|
||
}
|
||
|
||
// chessSettings — тот же самый экран выбора сложности, но для
|
||
// шахмат (свой уровень глубины поиска, т.к. ветвление в шахматах
|
||
// намного шире, чем в шашках).
|
||
type chessSettings struct {
|
||
levelIdx int
|
||
gameIdx int
|
||
}
|
||
|
||
var chessLevels = []struct {
|
||
LabelKey string
|
||
Depth int
|
||
DescKey string
|
||
}{
|
||
{LabelKey: "checkerslvl.easy", Depth: ChessDifficultyEasy, DescKey: "checkerslvl.easy_desc"},
|
||
{LabelKey: "checkerslvl.hard", Depth: ChessDifficultyHard, DescKey: "checkerslvl.hard_desc"},
|
||
}
|
||
|
||
func newChessSettings(gameIdx int) chessSettings {
|
||
return chessSettings{levelIdx: 0, gameIdx: gameIdx}
|
||
}
|
||
|
||
func (s *chessSettings) cycle(delta int) {
|
||
s.levelIdx += delta
|
||
if s.levelIdx < 0 {
|
||
s.levelIdx = 0
|
||
}
|
||
if s.levelIdx > len(chessLevels)-1 {
|
||
s.levelIdx = len(chessLevels) - 1
|
||
}
|
||
}
|
||
|
||
// goSettings — экран настройки партии в Го: два поля (размер доски
|
||
// и уровень сложности бота).
|
||
type goSettings struct {
|
||
sizeOptions []int
|
||
sizeIdx int
|
||
difficultyIdx int
|
||
fieldCursor int // 0 = размер доски, 1 = сложность
|
||
gameIdx int
|
||
}
|
||
|
||
var goDifficultyLevels = []struct {
|
||
LabelKey string
|
||
Level GoDifficulty
|
||
}{
|
||
{LabelKey: "gosettings.level.easy", Level: GoDifficultyEasy},
|
||
{LabelKey: "gosettings.level.medium", Level: GoDifficultyMedium},
|
||
{LabelKey: "gosettings.level.hard", Level: GoDifficultyHard},
|
||
}
|
||
|
||
func newGoSettings(gameIdx int) goSettings {
|
||
return goSettings{
|
||
sizeOptions: []int{9, 13, 19},
|
||
sizeIdx: 0,
|
||
difficultyIdx: 0,
|
||
gameIdx: gameIdx,
|
||
}
|
||
}
|
||
|
||
func (s goSettings) size() int { return s.sizeOptions[s.sizeIdx] }
|
||
func (s goSettings) difficulty() GoDifficulty { return goDifficultyLevels[s.difficultyIdx].Level }
|
||
|
||
func (s *goSettings) cycle(delta int) {
|
||
clamp := func(idx, max int) int {
|
||
idx += delta
|
||
if idx < 0 {
|
||
return 0
|
||
}
|
||
if idx > max {
|
||
return max
|
||
}
|
||
return idx
|
||
}
|
||
switch s.fieldCursor {
|
||
case 0:
|
||
s.sizeIdx = clamp(s.sizeIdx, len(s.sizeOptions)-1)
|
||
case 1:
|
||
s.difficultyIdx = clamp(s.difficultyIdx, len(goDifficultyLevels)-1)
|
||
}
|
||
}
|
||
|
||
// nardySettings — экран настройки партии в нарды: выбор варианта
|
||
// правил (короткие/длинные) и уровня сложности бота.
|
||
type nardySettings struct {
|
||
variantIdx int
|
||
difficultyIdx int
|
||
fieldCursor int // 0 = вариант правил, 1 = сложность
|
||
gameIdx int
|
||
}
|
||
|
||
var nardyVariantOptions = []struct {
|
||
LabelKey string
|
||
Variant NardyVariant
|
||
}{
|
||
{LabelKey: "nardysettings.variant.short", Variant: NardyShort},
|
||
{LabelKey: "nardysettings.variant.long", Variant: NardyLong},
|
||
}
|
||
|
||
var nardyDifficultyLevels = []struct {
|
||
LabelKey string
|
||
DescKey string
|
||
Difficulty NardyDifficulty
|
||
}{
|
||
{LabelKey: "nardylvl.easy", DescKey: "nardylvl.easy_desc", Difficulty: NardyDifficultyEasy},
|
||
{LabelKey: "nardylvl.medium", DescKey: "nardylvl.medium_desc", Difficulty: NardyDifficultyMedium},
|
||
{LabelKey: "nardylvl.hard", DescKey: "nardylvl.hard_desc", Difficulty: NardyDifficultyHard},
|
||
}
|
||
|
||
func newNardySettings(gameIdx int) nardySettings {
|
||
return nardySettings{gameIdx: gameIdx}
|
||
}
|
||
|
||
func (s nardySettings) variant() NardyVariant { return nardyVariantOptions[s.variantIdx].Variant }
|
||
func (s nardySettings) difficulty() NardyDifficulty {
|
||
return nardyDifficultyLevels[s.difficultyIdx].Difficulty
|
||
}
|
||
|
||
func (s *nardySettings) cycle(delta int) {
|
||
clamp := func(idx, max int) int {
|
||
idx += delta
|
||
if idx < 0 {
|
||
return 0
|
||
}
|
||
if idx > max {
|
||
return max
|
||
}
|
||
return idx
|
||
}
|
||
switch s.fieldCursor {
|
||
case 0:
|
||
s.variantIdx = clamp(s.variantIdx, len(nardyVariantOptions)-1)
|
||
case 1:
|
||
s.difficultyIdx = clamp(s.difficultyIdx, len(nardyDifficultyLevels)-1)
|
||
}
|
||
}
|
||
|
||
// urSettings — экран выбора уровня сложности бота перед партией в Ур.
|
||
type urSettings struct {
|
||
levelIdx int
|
||
gameIdx int
|
||
}
|
||
|
||
var urDifficultyLevels = []struct {
|
||
LabelKey string
|
||
DescKey string
|
||
Difficulty UrDifficulty
|
||
}{
|
||
{LabelKey: "urlvl.easy", DescKey: "urlvl.easy_desc", Difficulty: UrDifficultyEasy},
|
||
{LabelKey: "urlvl.medium", DescKey: "urlvl.medium_desc", Difficulty: UrDifficultyMedium},
|
||
{LabelKey: "urlvl.hard", DescKey: "urlvl.hard_desc", Difficulty: UrDifficultyHard},
|
||
}
|
||
|
||
func newUrSettings(gameIdx int) urSettings {
|
||
return urSettings{gameIdx: gameIdx}
|
||
}
|
||
|
||
func (s *urSettings) cycle(delta int) {
|
||
s.levelIdx += delta
|
||
if s.levelIdx < 0 {
|
||
s.levelIdx = 0
|
||
}
|
||
if s.levelIdx > len(urDifficultyLevels)-1 {
|
||
s.levelIdx = len(urDifficultyLevels) - 1
|
||
}
|
||
}
|
||
|
||
// senetSettings — экран выбора уровня сложности бота перед партией
|
||
// в Сенет.
|
||
type senetSettings struct {
|
||
levelIdx int
|
||
gameIdx int
|
||
}
|
||
|
||
var senetDifficultyLevels = []struct {
|
||
LabelKey string
|
||
DescKey string
|
||
Difficulty SenetDifficulty
|
||
}{
|
||
{LabelKey: "senetlvl.easy", DescKey: "senetlvl.easy_desc", Difficulty: SenetDifficultyEasy},
|
||
{LabelKey: "senetlvl.medium", DescKey: "senetlvl.medium_desc", Difficulty: SenetDifficultyMedium},
|
||
{LabelKey: "senetlvl.hard", DescKey: "senetlvl.hard_desc", Difficulty: SenetDifficultyHard},
|
||
}
|
||
|
||
func newSenetSettings(gameIdx int) senetSettings {
|
||
return senetSettings{gameIdx: gameIdx}
|
||
}
|
||
|
||
func (s *senetSettings) cycle(delta int) {
|
||
s.levelIdx += delta
|
||
if s.levelIdx < 0 {
|
||
s.levelIdx = 0
|
||
}
|
||
if s.levelIdx > len(senetDifficultyLevels)-1 {
|
||
s.levelIdx = len(senetDifficultyLevels) - 1
|
||
}
|
||
}
|
||
|
||
// minesweeperSettings — экран выбора пресета размера поля Сапёра.
|
||
type minesweeperSettings struct {
|
||
presetIdx int
|
||
gameIdx int
|
||
}
|
||
|
||
var minesweeperPresets = []struct {
|
||
LabelKey string
|
||
Width, Height, Mines int
|
||
}{
|
||
{"mssettings.beginner", 9, 9, 10},
|
||
{"mssettings.intermediate", 16, 16, 40},
|
||
{"mssettings.expert", 30, 16, 99},
|
||
}
|
||
|
||
func newMinesweeperSettings(gameIdx int) minesweeperSettings {
|
||
return minesweeperSettings{presetIdx: 0, gameIdx: gameIdx}
|
||
}
|
||
|
||
func (s *minesweeperSettings) cycle(delta int) {
|
||
s.presetIdx += delta
|
||
if s.presetIdx < 0 {
|
||
s.presetIdx = 0
|
||
}
|
||
if s.presetIdx > len(minesweeperPresets)-1 {
|
||
s.presetIdx = len(minesweeperPresets) - 1
|
||
}
|
||
}
|
||
|
||
type blackjackSettings struct {
|
||
capitalOptions []int
|
||
betOptions []int
|
||
|
||
playerCount int
|
||
capitalIdx int
|
||
betIdx int
|
||
fieldCursor int // 0 = число игроков, 1 = капитал, 2 = ставка
|
||
gameIdx int
|
||
}
|
||
|
||
func newBlackjackSettings(gameIdx int) blackjackSettings {
|
||
return blackjackSettings{
|
||
capitalOptions: []int{50, 100, 200, 500, 1000},
|
||
betOptions: []int{5, 10, 25, 50, 100},
|
||
playerCount: 1,
|
||
capitalIdx: 1, // 100
|
||
betIdx: 1, // 10
|
||
gameIdx: gameIdx,
|
||
}
|
||
}
|
||
|
||
func (s blackjackSettings) capital() int { return s.capitalOptions[s.capitalIdx] }
|
||
func (s blackjackSettings) bet() int { return s.betOptions[s.betIdx] }
|
||
|
||
func (s *blackjackSettings) cycle(delta int) {
|
||
clamp := func(idx, max int) int {
|
||
idx += delta
|
||
if idx < 0 {
|
||
return 0
|
||
}
|
||
if idx > max {
|
||
return max
|
||
}
|
||
return idx
|
||
}
|
||
switch s.fieldCursor {
|
||
case 0:
|
||
s.playerCount += delta
|
||
if s.playerCount < 1 {
|
||
s.playerCount = 1
|
||
}
|
||
if s.playerCount > 6 {
|
||
s.playerCount = 6
|
||
}
|
||
case 1:
|
||
s.capitalIdx = clamp(s.capitalIdx, len(s.capitalOptions)-1)
|
||
case 2:
|
||
s.betIdx = clamp(s.betIdx, len(s.betOptions)-1)
|
||
}
|
||
}
|
||
|
||
var (
|
||
menuTitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true)
|
||
menuSubtitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Italic(true)
|
||
menuItemStyle = lipgloss.NewStyle()
|
||
menuCursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("228")).Bold(true)
|
||
menuDescStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
||
menuHintStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Faint(true)
|
||
)
|
||
|
||
// menuOption — один пункт меню: либо запуск игры из реестра, либо
|
||
// служебное действие (правила/выход).
|
||
type menuOption struct {
|
||
kind string // "game", "rules", "quit"
|
||
gameIdx int // индекс в gameRegistry, если kind == "game"
|
||
}
|
||
|
||
// label возвращает актуальный (для текущего языка) текст пункта меню.
|
||
func (opt menuOption) label() string {
|
||
switch opt.kind {
|
||
case "game":
|
||
return gameRegistry[opt.gameIdx].Name()
|
||
case "rules":
|
||
return T("menu.rules")
|
||
case "language":
|
||
return T("menu.language")
|
||
case "quit":
|
||
return T("menu.quit")
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// desc возвращает актуальное (для текущего языка) описание пункта меню.
|
||
func (opt menuOption) desc() string {
|
||
switch opt.kind {
|
||
case "game":
|
||
return gameRegistry[opt.gameIdx].Description()
|
||
case "rules":
|
||
return T("menu.rules_desc")
|
||
case "language":
|
||
return T("menu.language_desc")
|
||
case "quit":
|
||
return T("menu.quit_desc")
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// AppModel — корневая модель приложения: меню, экран правил или
|
||
// запущенная игра.
|
||
type AppModel struct {
|
||
state appState
|
||
langCursor int
|
||
cursor int
|
||
menuScroll int // индекс первого видимого пункта главного меню
|
||
rulesCursor int
|
||
rulesMenuScroll int // индекс первого видимого пункта подменю правил
|
||
rulesScroll int // сколько строк текста правил прокручено вниз
|
||
termWidth int
|
||
termHeight int
|
||
options []menuOption
|
||
rulesText string
|
||
settings stakeSettings
|
||
playerCount playerCountSettings
|
||
blackjack blackjackSettings
|
||
checkers checkersSettings
|
||
ticTacToe ticTacToeSettings
|
||
nardy nardySettings
|
||
ur urSettings
|
||
senet senetSettings
|
||
chess chessSettings
|
||
goSetup goSettings
|
||
minesweeper minesweeperSettings
|
||
activeGame tea.Model
|
||
quitting bool
|
||
}
|
||
|
||
func NewAppModel() AppModel {
|
||
options := make([]menuOption, 0, len(gameRegistry)+3)
|
||
for i := range gameRegistry {
|
||
options = append(options, menuOption{kind: "game", gameIdx: i})
|
||
}
|
||
options = append(options, menuOption{kind: "rules"})
|
||
options = append(options, menuOption{kind: "language"})
|
||
options = append(options, menuOption{kind: "quit"})
|
||
|
||
return AppModel{
|
||
state: stateLanguage,
|
||
langCursor: int(CurrentLang()),
|
||
options: options,
|
||
termHeight: 24,
|
||
termWidth: 80,
|
||
}
|
||
}
|
||
|
||
func (m AppModel) Init() tea.Cmd {
|
||
return nil
|
||
}
|
||
|
||
func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
if sizeMsg, ok := msg.(tea.WindowSizeMsg); ok {
|
||
m.termWidth = sizeMsg.Width
|
||
m.termHeight = sizeMsg.Height
|
||
// на случай, если терминал реально изменили размером прямо во
|
||
// время сессии — курсор мог оказаться за пределами уже другого
|
||
// видимого окна, пересчитываем прокрутку под новую высоту
|
||
m.menuScroll = clampWindowScroll(m.cursor, m.menuScroll, m.menuVisibleItems(), len(m.options))
|
||
m.rulesMenuScroll = clampWindowScroll(m.rulesCursor, m.rulesMenuScroll, m.rulesMenuVisibleItems(), len(gameRegistry))
|
||
m.clampRulesScroll()
|
||
// пропускаем дальше — активная игра тоже может интересоваться
|
||
// размером окна в будущем; сейчас просто не теряем ход
|
||
if m.state == statePlaying && m.activeGame != nil {
|
||
updated, cmd := m.activeGame.Update(msg)
|
||
m.activeGame = updated
|
||
return m, cmd
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
switch m.state {
|
||
case statePlaying:
|
||
return m.updatePlaying(msg)
|
||
case stateLanguage:
|
||
return m.updateLanguage(msg)
|
||
case stateRulesMenu:
|
||
return m.updateRulesMenu(msg)
|
||
case stateRules:
|
||
return m.updateRules(msg)
|
||
case stateSettings:
|
||
return m.updateSettings(msg)
|
||
case statePlayerCount:
|
||
return m.updatePlayerCount(msg)
|
||
case stateBlackjackSettings:
|
||
return m.updateBlackjackSettings(msg)
|
||
case stateCheckersSettings:
|
||
return m.updateCheckersSettings(msg)
|
||
case stateTicTacToeSettings:
|
||
return m.updateTicTacToeSettings(msg)
|
||
case stateNardySettings:
|
||
return m.updateNardySettings(msg)
|
||
case stateUrSettings:
|
||
return m.updateUrSettings(msg)
|
||
case stateSenetSettings:
|
||
return m.updateSenetSettings(msg)
|
||
case stateChessSettings:
|
||
return m.updateChessSettings(msg)
|
||
case stateGoSettings:
|
||
return m.updateGoSettings(msg)
|
||
case stateMinesweeperSettings:
|
||
return m.updateMinesweeperSettings(msg)
|
||
default:
|
||
return m.updateMenu(msg)
|
||
}
|
||
}
|
||
|
||
func (m AppModel) updateLanguage(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "up", "k":
|
||
if m.langCursor > 0 {
|
||
m.langCursor--
|
||
}
|
||
case "down", "j":
|
||
if m.langCursor < 1 {
|
||
m.langCursor++
|
||
}
|
||
case "enter":
|
||
SetLanguage(Lang(m.langCursor))
|
||
m.state = stateMenu
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateMenu(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c", "q":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "up", "k":
|
||
if m.cursor > 0 {
|
||
m.cursor--
|
||
}
|
||
m.menuScroll = clampWindowScroll(m.cursor, m.menuScroll, m.menuVisibleItems(), len(m.options))
|
||
case "down", "j":
|
||
if m.cursor < len(m.options)-1 {
|
||
m.cursor++
|
||
}
|
||
m.menuScroll = clampWindowScroll(m.cursor, m.menuScroll, m.menuVisibleItems(), len(m.options))
|
||
case "enter":
|
||
selected := m.options[m.cursor]
|
||
switch selected.kind {
|
||
case "game":
|
||
entry := gameRegistry[selected.gameIdx]
|
||
switch {
|
||
case entry.UsesStakes:
|
||
m.settings = newStakeSettings(selected.gameIdx)
|
||
m.state = stateSettings
|
||
return m, nil
|
||
case entry.UsesPlayerCount:
|
||
m.playerCount = newPlayerCountSettings(selected.gameIdx)
|
||
m.state = statePlayerCount
|
||
return m, nil
|
||
case entry.UsesBlackjackSettings:
|
||
m.blackjack = newBlackjackSettings(selected.gameIdx)
|
||
m.state = stateBlackjackSettings
|
||
return m, nil
|
||
case entry.UsesCheckersSettings:
|
||
m.checkers = newCheckersSettings(selected.gameIdx)
|
||
m.state = stateCheckersSettings
|
||
return m, nil
|
||
case entry.UsesTicTacToeSettings:
|
||
m.ticTacToe = newTicTacToeSettings(selected.gameIdx)
|
||
m.state = stateTicTacToeSettings
|
||
return m, nil
|
||
case entry.UsesNardySettings:
|
||
m.nardy = newNardySettings(selected.gameIdx)
|
||
m.state = stateNardySettings
|
||
return m, nil
|
||
case entry.UsesUrSettings:
|
||
m.ur = newUrSettings(selected.gameIdx)
|
||
m.state = stateUrSettings
|
||
return m, nil
|
||
case entry.UsesSenetSettings:
|
||
m.senet = newSenetSettings(selected.gameIdx)
|
||
m.state = stateSenetSettings
|
||
return m, nil
|
||
case entry.UsesChessSettings:
|
||
m.chess = newChessSettings(selected.gameIdx)
|
||
m.state = stateChessSettings
|
||
return m, nil
|
||
case entry.UsesGoSettings:
|
||
m.goSetup = newGoSettings(selected.gameIdx)
|
||
m.state = stateGoSettings
|
||
return m, nil
|
||
case entry.UsesMinesweeperSettings:
|
||
m.minesweeper = newMinesweeperSettings(selected.gameIdx)
|
||
m.state = stateMinesweeperSettings
|
||
return m, nil
|
||
}
|
||
m.state = statePlaying
|
||
m.activeGame = entry.New()
|
||
return m, m.activeGame.Init()
|
||
case "rules":
|
||
m.rulesCursor = 0
|
||
m.rulesMenuScroll = 0
|
||
m.state = stateRulesMenu
|
||
case "language":
|
||
m.langCursor = int(CurrentLang())
|
||
m.state = stateLanguage
|
||
case "quit":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
}
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateRulesMenu(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
case "up", "k":
|
||
if m.rulesCursor > 0 {
|
||
m.rulesCursor--
|
||
}
|
||
m.rulesMenuScroll = clampWindowScroll(m.rulesCursor, m.rulesMenuScroll, m.rulesMenuVisibleItems(), len(gameRegistry))
|
||
case "down", "j":
|
||
if m.rulesCursor < len(gameRegistry)-1 {
|
||
m.rulesCursor++
|
||
}
|
||
m.rulesMenuScroll = clampWindowScroll(m.rulesCursor, m.rulesMenuScroll, m.rulesMenuVisibleItems(), len(gameRegistry))
|
||
case "enter":
|
||
m.rulesText = gameRegistry[m.rulesCursor].Rules()
|
||
m.rulesScroll = 0
|
||
m.state = stateRules
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
// clampWindowScroll подбирает минимальную корректировку scroll так,
|
||
// чтобы cursor гарантированно попадал в видимое окно
|
||
// [scroll, scroll+visibleCount) — используется и для списка игр в
|
||
// главном меню, и для подменю правил, чтобы курсор при навигации
|
||
// стрелками никогда не убегал за пределы экрана.
|
||
func clampWindowScroll(cursor, scroll, visibleCount, total int) int {
|
||
if visibleCount <= 0 {
|
||
return 0
|
||
}
|
||
if cursor < scroll {
|
||
scroll = cursor
|
||
}
|
||
if cursor >= scroll+visibleCount {
|
||
scroll = cursor - visibleCount + 1
|
||
}
|
||
if scroll < 0 {
|
||
scroll = 0
|
||
}
|
||
maxScroll := total - visibleCount
|
||
if maxScroll < 0 {
|
||
maxScroll = 0
|
||
}
|
||
if scroll > maxScroll {
|
||
scroll = maxScroll
|
||
}
|
||
return scroll
|
||
}
|
||
|
||
// asciiBannerLines — число строк в ASCII-баннере (для расчёта, сколько
|
||
// места остаётся под сам список пунктов меню).
|
||
func asciiBannerLines() int {
|
||
return strings.Count(asciiBanner, "\n") + 1
|
||
}
|
||
|
||
// menuBannerCompactThreshold — если высота терминала меньше этого
|
||
// значения, полноразмерный ASCII-баннер (13 строк) заменяется на
|
||
// компактную однострочную надпись — иначе на невысоких терминалах
|
||
// под сам список игр совсем не остаётся места.
|
||
const menuBannerCompactThreshold = 30
|
||
|
||
const menuBannerCompact = "=== GO GAMES COLLECTION ==="
|
||
|
||
// menuUsesCompactBanner решает, показывать ли компактный заголовок
|
||
// вместо полного ASCII-баннера при текущей высоте терминала.
|
||
func (m AppModel) menuUsesCompactBanner() bool {
|
||
return m.termHeight < menuBannerCompactThreshold
|
||
}
|
||
|
||
// menuBannerLineCount — сколько строк реально займёт заголовок меню
|
||
// (баннер или его компактная замена) при текущей высоте терминала.
|
||
func (m AppModel) menuBannerLineCount() int {
|
||
if m.menuUsesCompactBanner() {
|
||
return 1
|
||
}
|
||
return asciiBannerLines()
|
||
}
|
||
|
||
// menuVisibleItems — сколько пунктов главного меню помещается на
|
||
// экран одновременно. Каждый пункт занимает 2 строки (название +
|
||
// описание), плюс заголовок, подзаголовок, пустая строка и подсказка
|
||
// снизу — фиксированный "хвост" интерфейса.
|
||
func (m AppModel) menuVisibleItems() int {
|
||
overhead := m.menuBannerLineCount() + 4 + 2 // подзаголовок + 2 пустые строки + подсказка + место под ▲/▼
|
||
available := m.termHeight - overhead
|
||
items := available / 2
|
||
if items < 1 {
|
||
items = 1
|
||
}
|
||
return items
|
||
}
|
||
|
||
// rulesMenuVisibleItems — сколько пунктов подменю правил помещается
|
||
// на экран одновременно (по одной строке на пункт, без описания).
|
||
func (m AppModel) rulesMenuVisibleItems() int {
|
||
overhead := 4 + 2 // заголовок + пустая строка + пустая строка + подсказка + место под ▲/▼
|
||
available := m.termHeight - overhead
|
||
if available < 1 {
|
||
available = 1
|
||
}
|
||
return available
|
||
}
|
||
|
||
// rulesVisibleLines — сколько строк текста правил помещается на
|
||
// экран одновременно (с учётом заголовка и подсказки снизу).
|
||
func (m AppModel) rulesVisibleLines() int {
|
||
h := m.termHeight - 4
|
||
if h < 3 {
|
||
h = 3
|
||
}
|
||
return h
|
||
}
|
||
|
||
func (m AppModel) rulesTotalLines() int {
|
||
return len(strings.Split(m.rulesText, "\n"))
|
||
}
|
||
|
||
func (m AppModel) rulesMaxScroll() int {
|
||
max := m.rulesTotalLines() - m.rulesVisibleLines()
|
||
if max < 0 {
|
||
max = 0
|
||
}
|
||
return max
|
||
}
|
||
|
||
func (m *AppModel) clampRulesScroll() {
|
||
if m.rulesScroll < 0 {
|
||
m.rulesScroll = 0
|
||
}
|
||
if max := m.rulesMaxScroll(); m.rulesScroll > max {
|
||
m.rulesScroll = max
|
||
}
|
||
}
|
||
|
||
func (m AppModel) updateRules(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "q", "esc":
|
||
m.state = stateRulesMenu
|
||
return m, nil
|
||
case "up", "k":
|
||
m.rulesScroll--
|
||
case "down", "j":
|
||
m.rulesScroll++
|
||
case "pgup", "b":
|
||
m.rulesScroll -= m.rulesVisibleLines()
|
||
case "pgdown", "f", " ":
|
||
m.rulesScroll += m.rulesVisibleLines()
|
||
case "home", "g":
|
||
m.rulesScroll = 0
|
||
case "end", "G":
|
||
m.rulesScroll = m.rulesMaxScroll()
|
||
}
|
||
m.clampRulesScroll()
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updatePlayerCount(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "left", "h", "down", "j":
|
||
m.playerCount.cycle(-1)
|
||
case "right", "l", "up", "k":
|
||
m.playerCount.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.playerCount.gameIdx]
|
||
m.activeGame = entry.NewWithPlayerCount(m.playerCount.count)
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateBlackjackSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k":
|
||
if m.blackjack.fieldCursor > 0 {
|
||
m.blackjack.fieldCursor--
|
||
}
|
||
case "down", "j":
|
||
if m.blackjack.fieldCursor < 2 {
|
||
m.blackjack.fieldCursor++
|
||
}
|
||
case "left", "h":
|
||
m.blackjack.cycle(-1)
|
||
case "right", "l":
|
||
m.blackjack.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.blackjack.gameIdx]
|
||
m.activeGame = entry.NewBlackjackWithSettings(m.blackjack.playerCount, m.blackjack.capital(), m.blackjack.bet())
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateCheckersSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k", "left", "h":
|
||
m.checkers.cycle(-1)
|
||
case "down", "j", "right", "l":
|
||
m.checkers.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.checkers.gameIdx]
|
||
depth := checkersLevels[m.checkers.levelIdx].Depth
|
||
m.activeGame = entry.NewCheckersWithDifficulty(depth)
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateTicTacToeSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k", "left", "h":
|
||
m.ticTacToe.cycle(-1)
|
||
case "down", "j", "right", "l":
|
||
m.ticTacToe.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.ticTacToe.gameIdx]
|
||
difficulty := ticTacToeLevels[m.ticTacToe.levelIdx].Difficulty
|
||
m.activeGame = entry.NewTicTacToeWithDifficulty(difficulty)
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateNardySettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k":
|
||
if m.nardy.fieldCursor > 0 {
|
||
m.nardy.fieldCursor--
|
||
}
|
||
case "down", "j":
|
||
if m.nardy.fieldCursor < 1 {
|
||
m.nardy.fieldCursor++
|
||
}
|
||
case "left", "h":
|
||
m.nardy.cycle(-1)
|
||
case "right", "l":
|
||
m.nardy.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.nardy.gameIdx]
|
||
m.activeGame = entry.NewNardyWithSettings(m.nardy.variant(), m.nardy.difficulty())
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateUrSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k", "left", "h":
|
||
m.ur.cycle(-1)
|
||
case "down", "j", "right", "l":
|
||
m.ur.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.ur.gameIdx]
|
||
difficulty := urDifficultyLevels[m.ur.levelIdx].Difficulty
|
||
m.activeGame = entry.NewUrWithDifficulty(difficulty)
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateSenetSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k", "left", "h":
|
||
m.senet.cycle(-1)
|
||
case "down", "j", "right", "l":
|
||
m.senet.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.senet.gameIdx]
|
||
difficulty := senetDifficultyLevels[m.senet.levelIdx].Difficulty
|
||
m.activeGame = entry.NewSenetWithDifficulty(difficulty)
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateChessSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k", "left", "h":
|
||
m.chess.cycle(-1)
|
||
case "down", "j", "right", "l":
|
||
m.chess.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.chess.gameIdx]
|
||
depth := chessLevels[m.chess.levelIdx].Depth
|
||
m.activeGame = entry.NewChessWithDifficulty(depth)
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateGoSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k":
|
||
if m.goSetup.fieldCursor > 0 {
|
||
m.goSetup.fieldCursor--
|
||
}
|
||
case "down", "j":
|
||
if m.goSetup.fieldCursor < 1 {
|
||
m.goSetup.fieldCursor++
|
||
}
|
||
case "left", "h":
|
||
m.goSetup.cycle(-1)
|
||
case "right", "l":
|
||
m.goSetup.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.goSetup.gameIdx]
|
||
m.activeGame = entry.NewGoWithSettings(m.goSetup.size(), m.goSetup.difficulty())
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateMinesweeperSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k", "left", "h":
|
||
m.minesweeper.cycle(-1)
|
||
case "down", "j", "right", "l":
|
||
m.minesweeper.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.minesweeper.gameIdx]
|
||
p := minesweeperPresets[m.minesweeper.presetIdx]
|
||
m.activeGame = entry.NewMinesweeperWithPreset(p.Width, p.Height, p.Mines)
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
keyMsg, ok := msg.(tea.KeyMsg)
|
||
if !ok {
|
||
return m, nil
|
||
}
|
||
switch keyMsg.String() {
|
||
case "ctrl+c":
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
case "esc", "q":
|
||
m.state = stateMenu
|
||
return m, nil
|
||
case "up", "k":
|
||
if m.settings.fieldCursor > 0 {
|
||
m.settings.fieldCursor--
|
||
}
|
||
case "down", "j":
|
||
if m.settings.fieldCursor < 2 {
|
||
m.settings.fieldCursor++
|
||
}
|
||
case "left", "h":
|
||
m.settings.cycle(-1)
|
||
case "right", "l":
|
||
m.settings.cycle(1)
|
||
case "enter":
|
||
entry := gameRegistry[m.settings.gameIdx]
|
||
m.activeGame = entry.NewWithStakes(m.settings.capital(), m.settings.ante(), m.settings.multiplier())
|
||
m.state = statePlaying
|
||
return m, m.activeGame.Init()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updatePlaying(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
// Ctrl+C всегда выходит из всего приложения целиком, а не
|
||
// только из текущей игры.
|
||
if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == "ctrl+c" {
|
||
m.quitting = true
|
||
return m, tea.Quit
|
||
}
|
||
|
||
updated, cmd := m.activeGame.Update(msg)
|
||
m.activeGame = updated
|
||
|
||
backToMenu := false
|
||
switch tm := updated.(type) {
|
||
case Model:
|
||
backToMenu = tm.backToMenu
|
||
case DurakModel:
|
||
backToMenu = tm.backToMenu
|
||
case BlackjackModel:
|
||
backToMenu = tm.backToMenu
|
||
case OneOhOneModel:
|
||
backToMenu = tm.backToMenu
|
||
case ThousandModel:
|
||
backToMenu = tm.backToMenu
|
||
case KlondikeModel:
|
||
backToMenu = tm.backToMenu
|
||
case CheckersModel:
|
||
backToMenu = tm.backToMenu
|
||
case ChessModel:
|
||
backToMenu = tm.backToMenu
|
||
case CorpseStarchModel:
|
||
backToMenu = tm.backToMenu
|
||
case GoModel:
|
||
backToMenu = tm.backToMenu
|
||
case MinesweeperModel:
|
||
backToMenu = tm.backToMenu
|
||
case WayfarerModel:
|
||
backToMenu = tm.backToMenu
|
||
case TicTacToeModel:
|
||
backToMenu = tm.backToMenu
|
||
case NardyModel:
|
||
backToMenu = tm.backToMenu
|
||
case SenetModel:
|
||
backToMenu = tm.backToMenu
|
||
case UrModel:
|
||
backToMenu = tm.backToMenu
|
||
}
|
||
if backToMenu {
|
||
m.state = stateMenu
|
||
m.activeGame = nil
|
||
return m, nil
|
||
}
|
||
return m, cmd
|
||
}
|
||
|
||
func (m AppModel) View() string {
|
||
if m.quitting {
|
||
return T("common.goodbye")
|
||
}
|
||
switch m.state {
|
||
case statePlaying:
|
||
return m.activeGame.View()
|
||
case stateLanguage:
|
||
return m.viewLanguage()
|
||
case stateRulesMenu:
|
||
return m.viewRulesMenu()
|
||
case stateRules:
|
||
return m.viewRules()
|
||
case stateSettings:
|
||
return m.viewSettings()
|
||
case statePlayerCount:
|
||
return m.viewPlayerCount()
|
||
case stateBlackjackSettings:
|
||
return m.viewBlackjackSettings()
|
||
case stateCheckersSettings:
|
||
return m.viewCheckersSettings()
|
||
case stateTicTacToeSettings:
|
||
return m.viewTicTacToeSettings()
|
||
case stateNardySettings:
|
||
return m.viewNardySettings()
|
||
case stateUrSettings:
|
||
return m.viewUrSettings()
|
||
case stateSenetSettings:
|
||
return m.viewSenetSettings()
|
||
case stateChessSettings:
|
||
return m.viewChessSettings()
|
||
case stateGoSettings:
|
||
return m.viewGoSettings()
|
||
case stateMinesweeperSettings:
|
||
return m.viewMinesweeperSettings()
|
||
default:
|
||
return m.viewMenu()
|
||
}
|
||
}
|
||
|
||
func (m AppModel) viewLanguage() string {
|
||
var b strings.Builder
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(T("lang.title")))
|
||
fmt.Fprintln(&b)
|
||
|
||
options := []string{T("lang.ru"), T("lang.en")}
|
||
for i, opt := range options {
|
||
cursor := " "
|
||
label := menuItemStyle.Render(opt)
|
||
if i == m.langCursor {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
label = menuCursorStyle.Render(opt)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, label)
|
||
}
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("lang.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewMenu() string {
|
||
var b strings.Builder
|
||
if m.menuUsesCompactBanner() {
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(menuBannerCompact))
|
||
} else {
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(asciiBanner))
|
||
}
|
||
fmt.Fprintln(&b, menuSubtitleStyle.Render(T("app.subtitle")))
|
||
fmt.Fprintln(&b)
|
||
|
||
visible := m.menuVisibleItems()
|
||
start := m.menuScroll
|
||
end := start + visible
|
||
if end > len(m.options) {
|
||
end = len(m.options)
|
||
}
|
||
|
||
if start > 0 {
|
||
fmt.Fprintln(&b, dimStyle.Render(" ▲"))
|
||
}
|
||
for i := start; i < end; i++ {
|
||
opt := m.options[i]
|
||
cursor := " "
|
||
label := menuItemStyle.Render(opt.label())
|
||
if i == m.cursor {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
label = menuCursorStyle.Render(opt.label())
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, label)
|
||
if d := opt.desc(); d != "" {
|
||
fmt.Fprintf(&b, " %s\n", menuDescStyle.Render(d))
|
||
}
|
||
}
|
||
if end < len(m.options) {
|
||
fmt.Fprintln(&b, dimStyle.Render(" ▼"))
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("menu.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewRulesMenu() string {
|
||
var b strings.Builder
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(T("rulesmenu.title")))
|
||
fmt.Fprintln(&b)
|
||
|
||
visible := m.rulesMenuVisibleItems()
|
||
start := m.rulesMenuScroll
|
||
end := start + visible
|
||
if end > len(gameRegistry) {
|
||
end = len(gameRegistry)
|
||
}
|
||
|
||
if start > 0 {
|
||
fmt.Fprintln(&b, dimStyle.Render(" ▲"))
|
||
}
|
||
for i := start; i < end; i++ {
|
||
entry := gameRegistry[i]
|
||
cursor := " "
|
||
label := menuItemStyle.Render(entry.Name())
|
||
if i == m.rulesCursor {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
label = menuCursorStyle.Render(entry.Name())
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, label)
|
||
}
|
||
if end < len(gameRegistry) {
|
||
fmt.Fprintln(&b, dimStyle.Render(" ▼"))
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("rulesmenu.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewRules() string {
|
||
var b strings.Builder
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(T("rules.title")))
|
||
fmt.Fprintln(&b)
|
||
|
||
lines := strings.Split(m.rulesText, "\n")
|
||
visible := m.rulesVisibleLines()
|
||
start := m.rulesScroll
|
||
if start > len(lines) {
|
||
start = len(lines)
|
||
}
|
||
end := start + visible
|
||
if end > len(lines) {
|
||
end = len(lines)
|
||
}
|
||
fmt.Fprintln(&b, strings.Join(lines[start:end], "\n"))
|
||
|
||
maxScroll := m.rulesMaxScroll()
|
||
scrollInfo := ""
|
||
if maxScroll > 0 {
|
||
scrollInfo = " " + Tf("rules.scroll_info", start+1, end, len(lines))
|
||
}
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("rules.hint")+scrollInfo))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewSettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.settings.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("stake.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
fields := []struct {
|
||
label string
|
||
value string
|
||
}{
|
||
{T("stake.capital"), fmt.Sprintf("%d", m.settings.capital())},
|
||
{T("stake.ante"), fmt.Sprintf("%d", m.settings.ante())},
|
||
{T("stake.tonk_mult"), fmt.Sprintf("×%d", m.settings.multiplier())},
|
||
}
|
||
for i, f := range fields {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%-28s ← %s →", f.label, f.value)
|
||
if i == m.settings.fieldCursor {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
pot := m.settings.ante() * 4
|
||
fmt.Fprintf(&b, "%s\n", menuDescStyle.Render(fmt.Sprintf(
|
||
"%s: %d. %s: ×%d = %d.",
|
||
T("stake.pot_info"), pot, T("stake.tonk_win"), m.settings.multiplier(), pot*m.settings.multiplier())))
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("stake.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewPlayerCount() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.playerCount.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("playercount.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintf(&b, "%s\n", menuCursorStyle.Render(fmt.Sprintf(" ← %d %s →", m.playerCount.count, T("playercount.line"))))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintf(&b, "%s\n", menuDescStyle.Render(Tf("playercount.bots", m.playerCount.count-1)))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("playercount.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewBlackjackSettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.blackjack.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("bjsettings.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
fields := []struct {
|
||
label string
|
||
value string
|
||
}{
|
||
{T("bjsettings.players"), fmt.Sprintf("%d", m.blackjack.playerCount)},
|
||
{T("bjsettings.capital"), fmt.Sprintf("%d", m.blackjack.capital())},
|
||
{T("bjsettings.bet"), fmt.Sprintf("%d", m.blackjack.bet())},
|
||
}
|
||
for i, f := range fields {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%-30s ← %s →", f.label, f.value)
|
||
if i == m.blackjack.fieldCursor {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
botsCount := m.blackjack.playerCount - 1
|
||
if botsCount > 0 {
|
||
fmt.Fprintf(&b, "%s\n", menuDescStyle.Render(Tf("bjsettings.multi", botsCount)))
|
||
} else {
|
||
fmt.Fprintln(&b, menuDescStyle.Render(T("bjsettings.solo")))
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("bjsettings.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewCheckersSettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.checkers.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("checkerslvl.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
for i, lvl := range checkersLevels {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%s (%s)", T(lvl.LabelKey), T(lvl.DescKey))
|
||
if i == m.checkers.levelIdx {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("checkerslvl.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewTicTacToeSettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.ticTacToe.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("tictactoelvl.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
for i, lvl := range ticTacToeLevels {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%s (%s)", T(lvl.LabelKey), T(lvl.DescKey))
|
||
if i == m.ticTacToe.levelIdx {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("checkerslvl.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewNardySettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.nardy.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("nardysettings.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
fields := []struct {
|
||
label string
|
||
value string
|
||
}{
|
||
{T("nardysettings.variant"), T(nardyVariantOptions[m.nardy.variantIdx].LabelKey)},
|
||
{T("nardysettings.difficulty"), T(nardyDifficultyLevels[m.nardy.difficultyIdx].LabelKey)},
|
||
}
|
||
for i, f := range fields {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%-30s ← %s →", f.label, f.value)
|
||
if i == m.nardy.fieldCursor {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
desc := nardyDifficultyLevels[m.nardy.difficultyIdx].DescKey
|
||
fmt.Fprintln(&b, dimStyle.Render(T(desc)))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("gosettings.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewUrSettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.ur.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("checkerslvl.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
for i, lvl := range urDifficultyLevels {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%s (%s)", T(lvl.LabelKey), T(lvl.DescKey))
|
||
if i == m.ur.levelIdx {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("checkerslvl.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewSenetSettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.senet.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("checkerslvl.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
for i, lvl := range senetDifficultyLevels {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%s (%s)", T(lvl.LabelKey), T(lvl.DescKey))
|
||
if i == m.senet.levelIdx {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("checkerslvl.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewChessSettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.chess.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("checkerslvl.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
for i, lvl := range chessLevels {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%s (%s)", T(lvl.LabelKey), T(lvl.DescKey))
|
||
if i == m.chess.levelIdx {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("checkerslvl.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewGoSettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.goSetup.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("gosettings.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
fields := []struct {
|
||
label string
|
||
value string
|
||
}{
|
||
{T("gosettings.boardsize"), fmt.Sprintf("%dx%d", m.goSetup.size(), m.goSetup.size())},
|
||
{T("gosettings.difficulty"), T(goDifficultyLevels[m.goSetup.difficultyIdx].LabelKey)},
|
||
}
|
||
for i, f := range fields {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%-30s ← %s →", f.label, f.value)
|
||
if i == m.goSetup.fieldCursor {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("gosettings.hint")))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewMinesweeperSettings() string {
|
||
var b strings.Builder
|
||
entry := gameRegistry[m.minesweeper.gameIdx]
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("mssettings.title"), entry.Name())))
|
||
fmt.Fprintln(&b)
|
||
|
||
for i, p := range minesweeperPresets {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%s — %dx%d, %d 💣", T(p.LabelKey), p.Width, p.Height, p.Mines)
|
||
if i == m.minesweeper.presetIdx {
|
||
cursor = menuCursorStyle.Render("> ")
|
||
line = menuCursorStyle.Render(line)
|
||
}
|
||
fmt.Fprintf(&b, "%s%s\n", cursor, line)
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render(T("mssettings.hint")))
|
||
return b.String()
|
||
}
|