go_games_collection/wayfarer_galaxy.go

175 lines
8 KiB
Go
Raw Permalink 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"
"math"
"math/rand"
)
// ПРИМЕЧАНИЕ: "Странник" — текстовая космоторговля, вдохновлённая
// классической Elite (процедурная галактика, торговля, апгрейды
// корабля, риск в пути), но НЕ является её копией: собственный
// алгоритм генерации галактики (не побайтовое воспроизведение
// оригинального 6502-алгоритма), собственные названия кораблей,
// рас и систем, свой список квестов. Настоящего 3D-полёта в
// реальном времени нет — это сознательное решение: честная 3D-
// графика в терминале через Bubble Tea потребовала бы отдельного
// рендер-движка сопоставимой с самой игрой сложности. Вместо этого
// перелёты и опасные встречи разрешаются текстом с выбором.
// WayfarerSystemID — идентификатор звёздной системы: галактика +
// порядковый номер внутри неё. Все свойства системы выводятся из
// этого идентификатора детерминированно (как исходная генерация
// Elite) — хранить нужно только идентификатор, не сами данные.
type WayfarerSystemID struct {
Galaxy int
Index int
}
// MarshalText/UnmarshalText — encoding/json не умеет сериализовать
// map с ключом-структурой напрямую; эти методы позволяют
// WayfarerSystemID быть ключом map[WayfarerSystemID]bool (поле
// Visited) при сохранении карьеры в JSON.
func (id WayfarerSystemID) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("%d:%d", id.Galaxy, id.Index)), nil
}
func (id *WayfarerSystemID) UnmarshalText(text []byte) error {
_, err := fmt.Sscanf(string(text), "%d:%d", &id.Galaxy, &id.Index)
return err
}
const (
WayfarerGalaxyCount = 4
WayfarerSystemsPerGalaxy = 128
wayfarerGalaxyMapSize = 500.0 // условные световые годы на сторону карты галактики
)
// WayfarerGovernment — тип правительства системы: влияет на
// опасность перелётов (шанс встретить пиратов) и на легальность
// некоторых товаров.
type WayfarerGovernment int
const (
GovAnarchy WayfarerGovernment = iota
GovFeudal
GovDictatorship
GovCommunist
GovConfederacy
GovDemocracy
GovCorporateState
wayfarerGovernmentCount
)
// wayfarerGovernmentDangerFactor — во сколько раз обычная опасность
// пути умножается для системы с этим правительством (анархия
// заметно опаснее корпоративного государства).
var wayfarerGovernmentDangerFactor = map[WayfarerGovernment]float64{
GovAnarchy: 2.2,
GovFeudal: 1.5,
GovDictatorship: 1.3,
GovCommunist: 1.1,
GovConfederacy: 0.9,
GovDemocracy: 0.8,
GovCorporateState: 0.6,
}
// WayfarerSystem — производные (детерминированно вычисленные)
// данные одной звёздной системы.
type WayfarerSystem struct {
ID WayfarerSystemID
Name string
Government WayfarerGovernment
TechLevel int // 0-15
Industrial float64 // 0 (чисто аграрная) .. 1 (чисто промышленная)
Wealth float64 // 0 (бедная) .. 1 (богатая)
Population float64 // в миллионах, условно
X, Y float64 // координаты на карте галактики (условные св. годы)
}
// wayfarerGalaxySeed — необязательная "пасхалка": если рядом с
// исполняемым файлом (или в текущей рабочей директории) найден файл
// .env со строкой WAYFARER_GALAXY_SEED=<число>, вся галактика
// перегенерируется заново с другим содержимым (как изменение
// исходных чисел генератора в оригинальной Elite). По умолчанию 0 —
// это ничего не меняет в хэше (см. systemSeed) и даёт ту же самую
// галактику, что и без файла .env вовсе.
var wayfarerGalaxySeed uint64
// systemSeed детерминированно превращает идентификатор системы в
// 64-битное зерно генератора случайности — одна и та же система при
// одном и том же wayfarerGalaxySeed всегда даёт одни и те же данные.
func systemSeed(id WayfarerSystemID) int64 {
// простой, но достаточно хорошо перемешивающий детерминированный
// хэш (вариант splitmix64) — не претендует на криптостойкость,
// только на визуально несвязанный разброс результатов
x := uint64(id.Galaxy)*1000003 + uint64(id.Index) + 0x9E3779B97F4A7C15 + wayfarerGalaxySeed
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9
x = (x ^ (x >> 27)) * 0x94D049BB133111EB
x = x ^ (x >> 31)
return int64(x)
}
var wayfarerNameSyllables = []string{
"di", "so", "lav", "en", "ra", "or", "bi", "lei", "ce", "ta",
"zon", "quen", "ve", "usu", "phi", "gal", "ari", "nex", "tor", "eth",
"in", "us", "on", "ax", "yr", "ul", "im", "os", "an", "el",
}
// generateSystemName собирает произносимое "инопланетное" название
// из 2-3 слогов на основе персонального генератора системы.
func generateSystemName(rng *rand.Rand) string {
syllables := 2 + rng.Intn(2)
name := ""
for i := 0; i < syllables; i++ {
name += wayfarerNameSyllables[rng.Intn(len(wayfarerNameSyllables))]
}
return capitalizeFirst(name)
}
func capitalizeFirst(s string) string {
if s == "" {
return s
}
r := []rune(s)
if r[0] >= 'a' && r[0] <= 'z' {
r[0] = r[0] - 'a' + 'A'
}
return string(r)
}
// GenerateSystem детерминированно вычисляет все данные системы по
// её идентификатору.
func GenerateSystem(id WayfarerSystemID) WayfarerSystem {
rng := rand.New(rand.NewSource(systemSeed(id)))
return WayfarerSystem{
ID: id,
Name: generateSystemName(rng),
Government: WayfarerGovernment(rng.Intn(int(wayfarerGovernmentCount))),
TechLevel: rng.Intn(16),
Industrial: rng.Float64(),
Wealth: rng.Float64(),
Population: math.Round(rng.Float64()*450*10) / 10,
X: rng.Float64() * wayfarerGalaxyMapSize,
Y: rng.Float64() * wayfarerGalaxyMapSize,
}
}
// Distance — расстояние между двумя системами в условных световых
// годах (координаты считаются в пределах одной и той же галактики;
// межгалактический прыжок обрабатывается отдельно).
func (s WayfarerSystem) Distance(other WayfarerSystem) float64 {
dx := s.X - other.X
dy := s.Y - other.Y
return math.Sqrt(dx*dx + dy*dy)
}
// DangerFactor — во сколько раз обычная базовая опасность перелёта
// из/в эту систему умножается из-за её правительства.
func (s WayfarerSystem) DangerFactor() float64 {
return wayfarerGovernmentDangerFactor[s.Government]
}
func (s WayfarerSystem) String() string {
return fmt.Sprintf("%s (G%d/%d)", s.Name, s.ID.Galaxy, s.ID.Index)
}