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) }