772 lines
24 KiB
Go
772 lines
24 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
// GameEntry — запись в реестре игр коллекции. Регистр сделан так,
|
||
// чтобы добавление новой игры сводилось к добавлению одной записи
|
||
// в gameRegistry, без изменений в логике меню.
|
||
type GameEntry struct {
|
||
Name string
|
||
Description string
|
||
Rules 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
|
||
}
|
||
|
||
// gameRegistry — список доступных игр коллекции.
|
||
var gameRegistry = []GameEntry{
|
||
{
|
||
Name: "Тонк (Tonk)",
|
||
Description: "Карточная игра на 1 человека + 3 бота разного уровня сложности",
|
||
Rules: tonkRules,
|
||
New: func() tea.Model { return NewModel() },
|
||
UsesStakes: true,
|
||
NewWithStakes: func(capital, ante, mult int) tea.Model { return NewModelWithStakes(capital, ante, mult) },
|
||
},
|
||
{
|
||
Name: "Дурак (Durak)",
|
||
Description: "Классическая карточная игра на 2-6 игроков (подкидной вариант)",
|
||
Rules: durakRules,
|
||
New: func() tea.Model { return NewDurakModel(4) },
|
||
UsesPlayerCount: true,
|
||
NewWithPlayerCount: func(n int) tea.Model { return NewDurakModel(n) },
|
||
},
|
||
{
|
||
Name: "Блэкджек (Blackjack)",
|
||
Description: "Каждый играет против дилера, 1-6 игроков включая человека",
|
||
Rules: blackjackRules,
|
||
UsesBlackjackSettings: true,
|
||
NewBlackjackWithSettings: func(numPlayers, capital, bet int) tea.Model {
|
||
return NewBlackjackModel(numPlayers, capital, bet)
|
||
},
|
||
},
|
||
{
|
||
Name: "101",
|
||
Description: "Избавьтесь от карт первым; штрафные очки за оставшиеся на руках, до 101 — выбывание",
|
||
Rules: oneOhOneRules,
|
||
New: func() tea.Model { return NewOneOhOneModel(4) },
|
||
UsesPlayerCount: true,
|
||
NewWithPlayerCount: func(n int) tea.Model { return NewOneOhOneModel(n) },
|
||
},
|
||
{
|
||
Name: "1000 (Тысяча)",
|
||
Description: "Взяточная игра с торгами и марьяжами, строго на троих (вы + 2 бота)",
|
||
Rules: thousandRules,
|
||
New: func() tea.Model { return NewThousandModel() },
|
||
},
|
||
{
|
||
Name: "Косынка (Klondike)",
|
||
Description: "Классический пасьянс на одного игрока, без ботов",
|
||
Rules: klondikeRules,
|
||
New: func() tea.Model { return NewKlondikeModel() },
|
||
},
|
||
{
|
||
Name: "Шашки (Checkers)",
|
||
Description: "Русские шашки против бота (минимакс), обязательное взятие",
|
||
Rules: checkersRules,
|
||
UsesCheckersSettings: true,
|
||
NewCheckersWithDifficulty: func(depth int) tea.Model {
|
||
return NewCheckersModel(depth)
|
||
},
|
||
},
|
||
}
|
||
|
||
type appState int
|
||
|
||
const (
|
||
stateMenu appState = iota
|
||
stateRulesMenu
|
||
stateRules
|
||
stateSettings
|
||
statePlayerCount
|
||
stateBlackjackSettings
|
||
stateCheckersSettings
|
||
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 {
|
||
Label string
|
||
Depth int
|
||
Desc string
|
||
}{
|
||
{Label: "Простой", Depth: CheckersDifficultyEasy, Desc: "быстрый, ощутимо слабее"},
|
||
{Label: "Сильный", Depth: CheckersDifficultyHard, 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
|
||
}
|
||
}
|
||
|
||
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 {
|
||
label string
|
||
desc string
|
||
kind string // "game", "rules", "quit"
|
||
gameIdx int // индекс в gameRegistry, если kind == "game"
|
||
}
|
||
|
||
// AppModel — корневая модель приложения: меню, экран правил или
|
||
// запущенная игра.
|
||
type AppModel struct {
|
||
state appState
|
||
cursor int
|
||
rulesCursor int
|
||
options []menuOption
|
||
rulesText string
|
||
settings stakeSettings
|
||
playerCount playerCountSettings
|
||
blackjack blackjackSettings
|
||
checkers checkersSettings
|
||
activeGame tea.Model
|
||
quitting bool
|
||
}
|
||
|
||
func NewAppModel() AppModel {
|
||
options := make([]menuOption, 0, len(gameRegistry)+2)
|
||
for i, g := range gameRegistry {
|
||
options = append(options, menuOption{label: g.Name, desc: g.Description, kind: "game", gameIdx: i})
|
||
}
|
||
options = append(options, menuOption{label: "Правила", desc: "Прочитать правила выбранной игры", kind: "rules"})
|
||
options = append(options, menuOption{label: "Выход", desc: "Закрыть коллекцию игр", kind: "quit"})
|
||
|
||
return AppModel{
|
||
state: stateMenu,
|
||
options: options,
|
||
}
|
||
}
|
||
|
||
func (m AppModel) Init() tea.Cmd {
|
||
return nil
|
||
}
|
||
|
||
func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
switch m.state {
|
||
case statePlaying:
|
||
return m.updatePlaying(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)
|
||
default:
|
||
return m.updateMenu(msg)
|
||
}
|
||
}
|
||
|
||
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--
|
||
}
|
||
case "down", "j":
|
||
if m.cursor < len(m.options)-1 {
|
||
m.cursor++
|
||
}
|
||
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
|
||
}
|
||
m.state = statePlaying
|
||
m.activeGame = entry.New()
|
||
return m, m.activeGame.Init()
|
||
case "rules":
|
||
m.rulesCursor = 0
|
||
m.state = stateRulesMenu
|
||
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--
|
||
}
|
||
case "down", "j":
|
||
if m.rulesCursor < len(gameRegistry)-1 {
|
||
m.rulesCursor++
|
||
}
|
||
case "enter":
|
||
m.rulesText = gameRegistry[m.rulesCursor].Rules
|
||
m.state = stateRules
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m AppModel) updateRules(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||
switch keyMsg.String() {
|
||
case "q", "esc", "enter", "ctrl+c":
|
||
m.state = stateRulesMenu
|
||
}
|
||
}
|
||
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) 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
|
||
}
|
||
if backToMenu {
|
||
m.state = stateMenu
|
||
m.activeGame = nil
|
||
return m, nil
|
||
}
|
||
return m, cmd
|
||
}
|
||
|
||
func (m AppModel) View() string {
|
||
if m.quitting {
|
||
return "До встречи!\n"
|
||
}
|
||
switch m.state {
|
||
case statePlaying:
|
||
return m.activeGame.View()
|
||
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()
|
||
default:
|
||
return m.viewMenu()
|
||
}
|
||
}
|
||
|
||
func (m AppModel) viewMenu() string {
|
||
var b strings.Builder
|
||
fmt.Fprintln(&b, menuTitleStyle.Render(asciiBanner))
|
||
fmt.Fprintln(&b, menuSubtitleStyle.Render("Коллекция консольных игр на Go"))
|
||
fmt.Fprintln(&b)
|
||
|
||
for i, opt := range m.options {
|
||
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 opt.desc != "" {
|
||
fmt.Fprintf(&b, " %s\n", menuDescStyle.Render(opt.desc))
|
||
}
|
||
}
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render("[↑↓/kj] выбор [enter] подтвердить [q] выход"))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewRulesMenu() string {
|
||
var b strings.Builder
|
||
fmt.Fprintln(&b, menuTitleStyle.Render("=== ПРАВИЛА: ВЫБЕРИТЕ ИГРУ ==="))
|
||
fmt.Fprintln(&b)
|
||
for i, entry := range gameRegistry {
|
||
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)
|
||
}
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render("[↑↓/kj] выбор [enter] показать правила [esc/q] назад в меню"))
|
||
return b.String()
|
||
}
|
||
|
||
func (m AppModel) viewRules() string {
|
||
var b strings.Builder
|
||
fmt.Fprintln(&b, menuTitleStyle.Render("=== ПРАВИЛА ==="))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, m.rulesText)
|
||
fmt.Fprintln(&b, menuHintStyle.Render("[enter/esc/q] назад к списку игр"))
|
||
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 ===", entry.Name)))
|
||
fmt.Fprintln(&b)
|
||
|
||
fields := []struct {
|
||
label string
|
||
value string
|
||
}{
|
||
{"Стартовый капитал", fmt.Sprintf("%d", m.settings.capital())},
|
||
{"Ставка (анте) за раунд", fmt.Sprintf("%d", m.settings.ante())},
|
||
{"Множитель банка при Тонке", 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(
|
||
"Банк на 4 игроков: %d. Победа Тонком: ×%d = %d.",
|
||
pot, m.settings.multiplier(), pot*m.settings.multiplier())))
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render("[↑↓/kj] поле [←→/hl] изменить значение [enter] начать игру [esc/q] назад"))
|
||
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 ===", entry.Name)))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintf(&b, "%s\n", menuCursorStyle.Render(fmt.Sprintf(" ← %d игроков (включая вас) →", m.playerCount.count)))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintf(&b, "%s\n", menuDescStyle.Render(fmt.Sprintf("Вы + %d бот(ов) со случайными именами.", m.playerCount.count-1)))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render("[←→/hl] изменить число игроков (2-6) [enter] начать игру [esc/q] назад"))
|
||
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 ===", entry.Name)))
|
||
fmt.Fprintln(&b)
|
||
|
||
fields := []struct {
|
||
label string
|
||
value string
|
||
}{
|
||
{"Число игроков (включая вас)", fmt.Sprintf("%d", m.blackjack.playerCount)},
|
||
{"Стартовый капитал", fmt.Sprintf("%d", m.blackjack.capital())},
|
||
{"Ставка за раунд", 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(fmt.Sprintf("Вы + %d бот(ов) со случайными именами, каждый играет против дилера отдельно.", botsCount)))
|
||
} else {
|
||
fmt.Fprintln(&b, menuDescStyle.Render("Вы играете в одиночку против дилера."))
|
||
}
|
||
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, menuHintStyle.Render("[↑↓/kj] поле [←→/hl] изменить значение [enter] начать игру [esc/q] назад"))
|
||
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 ===", entry.Name)))
|
||
fmt.Fprintln(&b)
|
||
|
||
for i, lvl := range checkersLevels {
|
||
cursor := " "
|
||
line := fmt.Sprintf("%s (%s)", lvl.Label, lvl.Desc)
|
||
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("[↑↓/←→] выбор уровня [enter] начать игру [esc/q] назад"))
|
||
return b.String()
|
||
}
|