go_games_collection/main.go

290 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"fmt"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
loadWayfarerGalaxySeed()
if len(os.Args) > 1 {
switch os.Args[1] {
case "-h", "--help":
SetLanguage(LangRU)
printHelp()
return
case "--help-en":
SetLanguage(LangEN)
printHelp()
return
case "-sim":
runSimulation()
return
case "-sim-durak":
runDurakSimulation()
return
case "-sim-blackjack":
runBlackjackSimulation()
return
case "-sim-101":
runOneOhOneSimulation()
return
case "-sim-1000":
runThousandSimulation()
return
}
}
p := tea.NewProgram(NewAppModel())
if _, err := p.Run(); err != nil {
fmt.Printf("ошибка TUI: %v\n", err)
os.Exit(1)
}
}
// printHelp выводит общее описание запуска и правила всех игр из
// реестра на текущем языке (см. SetLanguage) — доступно без захода
// в TUI, годится для чтения через `go-games-collection --help |
// less` или на непривычной к TUI системе.
func printHelp() {
fmt.Println(asciiBanner)
fmt.Println(T("app.subtitle"))
fmt.Println()
if CurrentLang() == LangEN {
fmt.Println("USAGE")
fmt.Println(" go-games-collection open the collection menu")
fmt.Println(" go-games-collection -sim headless Tonk bot simulation (no TUI)")
fmt.Println(" go-games-collection -sim-durak same, for Durak")
fmt.Println(" go-games-collection -sim-blackjack same, for Blackjack")
fmt.Println(" go-games-collection -sim-101 same, for 101")
fmt.Println(" go-games-collection -sim-1000 same, for 1000 (Thousand)")
fmt.Println(" go-games-collection --help show this help and rules in Russian")
fmt.Println(" go-games-collection --help-en show this help and rules in English")
} else {
fmt.Println("ЗАПУСК")
fmt.Println(" go-games-collection открыть меню коллекции")
fmt.Println(" go-games-collection -sim headless-симуляция ботов Тонка (без TUI)")
fmt.Println(" go-games-collection -sim-durak headless-симуляция ботов Дурака (без TUI)")
fmt.Println(" go-games-collection -sim-blackjack headless-симуляция ботов Блэкджека (без TUI)")
fmt.Println(" go-games-collection -sim-101 headless-симуляция ботов 101 (без TUI)")
fmt.Println(" go-games-collection -sim-1000 headless-симуляция ботов 1000 (без TUI)")
fmt.Println(" go-games-collection --help показать эту справку и правила на русском")
fmt.Println(" go-games-collection --help-en показать эту справку и правила на английском")
}
fmt.Println()
for _, g := range gameRegistry {
name := g.Name()
separator := strings.Repeat("=", len(name)+4)
fmt.Println(separator)
fmt.Printf(" %s\n", name)
fmt.Println(separator)
fmt.Println()
fmt.Println(g.Rules())
}
}
// runSimulation — headless-прогон партии из 4 ботов разного
// уровня, без TUI. Полезно для быстрой проверки баланса и
// поведения ботов: `go run . -sim`.
func runSimulation() {
names, bots := newBotRoster(4)
g := NewGame(names, -1)
const maxTurns = 500
for turn := 0; turn < maxTurns && g.Phase != PhaseGameOver; turn++ {
bot := bots[g.CurrentPlayer]
if err := bot.PlayTurn(g); err != nil {
fmt.Printf("ошибка на ходу %s: %v\n", g.Players[g.CurrentPlayer].Name, err)
break
}
}
if g.Result == nil {
fmt.Println("партия не завершилась за отведённое число ходов")
return
}
switch {
case g.Result.DeadRound:
fmt.Printf("раунд закончился вничью: колода и сброс исчерпаны (банк %d возвращён игрокам)\n", g.Result.Pot)
case g.Result.TonkCall:
fmt.Printf("%s объявил(а) Тонк и забрал(а) банк ×%d (%d)!\n", g.Players[g.Result.Winner].Name, g.TonkMultiplier, g.Result.Pot*g.TonkMultiplier)
default:
fmt.Printf("%s избавился(лась) от всех карт и забрал(а) банк (%d)!\n", g.Players[g.Result.Winner].Name, g.Result.Pot)
}
for i, p := range g.Players {
fmt.Printf(" %-20s изменение капитала: %+d (итого: %d)\n", p.Name, g.Result.BalanceDeltas[i], p.Balance)
}
}
// runDurakSimulation — headless-прогон партии Дурака ботами, без
// TUI: `go run . -sim-durak`. Число игроков фиксировано (4) для
// простоты; для проверки других раскладов используйте TUI.
func runDurakSimulation() {
names := newDurakBotNames(4)
g := NewDurakGame(names, -1)
bot := DurakBot{}
const maxSteps = 10000
steps := 0
for ; steps < maxSteps && g.Phase != DurakPhaseGameOver; steps++ {
if err := bot.PlayFullTurn(g); err != nil {
fmt.Printf("ошибка на шаге %d: %v\n", steps, err)
return
}
}
if g.Result == nil {
fmt.Println("партия не завершилась за отведённое число шагов")
return
}
switch {
case g.Result.Stalemate:
fmt.Println("партия зашла в тупик и принудительно завершена вничью")
case g.Result.NoLoser:
fmt.Println("ничья: все избавились от карт одновременно")
default:
fmt.Printf("%s остался(лась) с картами на руках — дурак!\n", g.Players[g.Result.Loser].Name)
}
for _, p := range g.Players {
status := "в игре"
if p.Out {
status = "отбился"
}
fmt.Printf(" %-10s карт: %-2d (%s)\n", p.Name, len(p.Hand), status)
}
}
// runBlackjackSimulation — headless-прогон нескольких раундов
// блэкджека ботами, без TUI: `go run . -sim-blackjack`. Число
// игроков фиксировано (3) для простоты.
func runBlackjackSimulation() {
names, _ := newBotRoster(3)
g := NewBlackjackGame(names, -1, 1000, 10)
bot := BlackjackBot{}
const rounds = 5
for round := 0; round < rounds; round++ {
const maxSteps = 100
for steps := 0; steps < maxSteps && g.Phase != BJPhaseRoundOver; steps++ {
if err := bot.PlayFullTurn(g); err != nil {
fmt.Printf("ошибка на шаге %d: %v\n", steps, err)
return
}
}
if g.Result == nil {
fmt.Println("раунд не завершился за отведённое число шагов")
return
}
fmt.Printf("--- Раунд %d --- дилер: %d", round+1, g.Result.DealerTotal)
if g.Result.DealerBusted {
fmt.Print(" (перебор)")
}
fmt.Println()
for i, p := range g.Players {
r := g.Result.PlayerResults[i]
fmt.Printf(" %-10s %-10s изменение капитала: %+d (итого: %d)\n", p.Name, r.Outcome, r.Delta, p.Balance)
}
if round < rounds-1 {
g = g.NextRound()
}
}
}
// runOneOhOneSimulation — headless-прогон полного матча 101
// ботами, без TUI: `go run . -sim-101`. Число игроков фиксировано
// (4) для простоты.
func runOneOhOneSimulation() {
names := newOneOhOneBotNames(4)
g := NewOneOhOneGame(names, -1)
bot := OneOhOneBot{}
const maxRounds = 200
for round := 0; round < maxRounds; round++ {
const maxSteps = 6000
steps := 0
for ; steps < maxSteps && g.Phase != OneOhOnePhaseRoundOver; steps++ {
if err := bot.PlayFullTurn(g); err != nil {
fmt.Printf("ошибка на шаге %d: %v\n", steps, err)
return
}
}
if g.Result == nil {
fmt.Println("раунд не завершился за отведённое число шагов")
return
}
if g.Result.Stalemate {
fmt.Printf("--- Раунд %d: тупик, счёт не изменился ---\n", round+1)
} else {
winner := g.Players[g.Result.Winner].Name
fmt.Printf("--- Раунд %d: %s избавился(лась) от карт первым(ой)", round+1, winner)
if g.Result.QueenBonus {
fmt.Print(" (бонус за даму!)")
}
fmt.Println()
for i, p := range g.Players {
fmt.Printf(" %-10s %+-4d очков за раунд (итого: %d)\n", p.Name, g.Result.ScoreDeltas[i], p.Score)
}
}
if g.Result.MatchOver {
if g.Result.NoWinner {
fmt.Println("Партия завершилась без победителя.")
} else {
fmt.Printf("Партия окончена! Победитель: %s\n", g.Players[g.Result.MatchWinner].Name)
}
return
}
g.NextRound()
}
fmt.Println("матч не завершился за отведённое число раундов")
}
// runThousandSimulation — headless-прогон полного матча 1000
// ботами, без TUI: `go run . -sim-1000`.
func runThousandSimulation() {
names := newThousandBotNames(3)
g := NewThousandGame(names, -1)
bot := ThousandBot{}
const maxHands = 500
for hand := 0; hand < maxHands; hand++ {
const maxSteps = 2000
steps := 0
for ; steps < maxSteps && g.Phase != ThousandPhaseHandOver; steps++ {
if err := bot.PlayFullTurn(g); err != nil {
fmt.Printf("ошибка на шаге %d (фаза %v): %v\n", steps, g.Phase, err)
return
}
}
if g.Result == nil {
fmt.Println("раздача не завершилась за отведённое число шагов")
return
}
declarer := g.Players[g.Result.DeclarerIdx].Name
status := "выполнил(а)"
if !g.Result.Made {
status = "не выполнил(а)"
}
fmt.Printf("--- Раздача %d: %s %s ставку %d ---\n", hand+1, declarer, status, g.Result.FinalBid)
for i, p := range g.Players {
fmt.Printf(" %-10s %+-5d очков (итого: %d)\n", p.Name, g.Result.ScoreDeltas[i], p.Score)
}
if g.Result.MatchOver {
fmt.Printf("Партия окончена! Победитель: %s\n", g.Players[g.Result.MatchWinner].Name)
return
}
g.NextHand()
}
fmt.Println("матч не завершился за отведённое число раздач")
}