175 lines
8 KiB
Go
175 lines
8 KiB
Go
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)
|
||
}
|