package main import ( "os" "path/filepath" "testing" ) func intPtr(n int) *int { return &n } func validYAMLQuest() wayfarerQuestYAML { return wayfarerQuestYAML{ Key: "quest.test_valid", Title: wfLocalizedText{RU: "Тестовый квест", EN: "Test Quest"}, Brief: wfLocalizedText{RU: "Краткое описание", EN: "Brief description"}, Steps: []wayfarerQuestStepYAML{ { Text: wfLocalizedText{RU: "Ситуация на шаге 0", EN: "Situation at step 0"}, Choices: []wayfarerQuestChoiceYAML{ {Label: wfLocalizedText{RU: "Идти дальше", EN: "Go on"}, NextStep: intPtr(1)}, {Label: wfLocalizedText{RU: "Отступить", EN: "Retreat"}, Terminal: true, Success: false, Result: wfLocalizedText{RU: "Провал", EN: "Failure"}}, }, }, { Text: wfLocalizedText{RU: "Ситуация на шаге 1", EN: "Situation at step 1"}, Choices: []wayfarerQuestChoiceYAML{ {Label: wfLocalizedText{RU: "Завершить", EN: "Finish"}, Terminal: true, Success: true, Result: wfLocalizedText{RU: "Успех", EN: "Success"}, CreditsDelta: 500, HullDelta: -5}, }, }, }, } } func TestConvertYAMLQuestValid(t *testing.T) { def, texts, err := convertYAMLQuest(validYAMLQuest(), "test.yaml") if err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if def.Key != "quest.test_valid" { t.Errorf("неверный ключ квеста: %q", def.Key) } if len(def.Steps) != 2 { t.Fatalf("ожидалось 2 шага, получено %d", len(def.Steps)) } if def.Steps[0].Choices[0].NextStep != 1 { t.Errorf("ожидался NextStep=1 у первого выбора") } if def.Steps[0].Choices[1].Terminal != true || def.Steps[0].Choices[1].Success != false { t.Errorf("второй выбор должен быть терминальным провалом") } if def.Steps[1].Choices[0].CreditsDelta != 500 || def.Steps[1].Choices[0].HullDelta != -5 { t.Errorf("не перенеслись числовые последствия выбора") } if def.Steps[0].Choices[0].CargoIdx != -1 { t.Errorf("ожидался CargoIdx=-1 по умолчанию, получено %d", def.Steps[0].Choices[0].CargoIdx) } if len(texts) == 0 { t.Errorf("ожидались тексты для регистрации переводов") } } func TestConvertYAMLQuestMissingKey(t *testing.T) { y := validYAMLQuest() y.Key = "" if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil { t.Errorf("ожидалась ошибка при отсутствующем key") } } func TestConvertYAMLQuestNoSteps(t *testing.T) { y := validYAMLQuest() y.Steps = nil if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil { t.Errorf("ожидалась ошибка при отсутствии шагов") } } func TestConvertYAMLQuestNoChoicesInStep(t *testing.T) { y := validYAMLQuest() y.Steps[0].Choices = nil if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil { t.Errorf("ожидалась ошибка при отсутствии вариантов выбора на шаге") } } func TestConvertYAMLQuestMissingNextStepOnNonTerminal(t *testing.T) { y := validYAMLQuest() y.Steps[0].Choices[0].NextStep = nil if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil { t.Errorf("ожидалась ошибка при отсутствии next_step у нетерминального выбора") } } func TestConvertYAMLQuestNextStepOutOfRange(t *testing.T) { y := validYAMLQuest() y.Steps[0].Choices[0].NextStep = intPtr(99) if _, _, err := convertYAMLQuest(y, "test.yaml"); err == nil { t.Errorf("ожидалась ошибка при next_step вне диапазона") } } func TestConvertYAMLQuestExplicitCargoIdx(t *testing.T) { y := validYAMLQuest() y.Steps[1].Choices[0].CargoIdx = intPtr(2) y.Steps[1].Choices[0].CargoDelta = -3 def, _, err := convertYAMLQuest(y, "test.yaml") if err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if def.Steps[1].Choices[0].CargoIdx != 2 || def.Steps[1].Choices[0].CargoDelta != -3 { t.Errorf("явно указанный cargo_idx/cargo_delta не сохранился") } } func writeYAMLFile(t *testing.T, dir, name, content string) { t.Helper() if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { t.Fatalf("не удалось записать тестовый файл: %v", err) } } func TestLoadWayfarerQuestsFromDirMissingDirIsHarmless(t *testing.T) { loaded, warnings := LoadWayfarerQuestsFromDir(filepath.Join(t.TempDir(), "does-not-exist"), nil) if loaded != nil || warnings != nil { t.Errorf("отсутствующая директория должна давать (nil, nil), получено (%v, %v)", loaded, warnings) } } func TestLoadWayfarerQuestsFromDirValidFile(t *testing.T) { dir := t.TempDir() writeYAMLFile(t, dir, "myquest.yaml", ` key: quest.loader_test_ok title: {ru: "Загруженный квест", en: "Loaded Quest"} brief: {ru: "Описание", en: "Brief"} steps: - text: {ru: "Шаг 0", en: "Step 0"} choices: - label: {ru: "Дальше", en: "Onward"} next_step: 1 - text: {ru: "Шаг 1", en: "Step 1"} choices: - label: {ru: "Конец", en: "The end"} terminal: true success: true result: {ru: "Победа", en: "Victory"} `) loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil) if len(warnings) != 0 { t.Errorf("не ожидалось предупреждений, получено: %v", warnings) } if len(loaded) != 1 { t.Fatalf("ожидался ровно 1 загруженный квест, получено %d", len(loaded)) } if loaded[0].Key != "quest.loader_test_ok" { t.Errorf("неверный ключ загруженного квеста: %q", loaded[0].Key) } if T("quest.loader_test_ok.title") != "Загруженный квест" { t.Errorf("перевод названия не зарегистрировался в общей карте переводов, T() вернул: %q", T("quest.loader_test_ok.title")) } } func TestLoadWayfarerQuestsFromDirSkipsMalformedYAML(t *testing.T) { dir := t.TempDir() writeYAMLFile(t, dir, "broken.yaml", "key: [это не валидный YAML для этой структуры\n - вложенность сломана") loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil) if len(loaded) != 0 { t.Errorf("сломанный файл не должен был загрузиться, получено %d квестов", len(loaded)) } if len(warnings) != 1 { t.Fatalf("ожидалось ровно 1 предупреждение, получено %d: %v", len(warnings), warnings) } } func TestLoadWayfarerQuestsFromDirSkipsDuplicateKey(t *testing.T) { dir := t.TempDir() writeYAMLFile(t, dir, "dup.yaml", ` key: quest.supernova title: {ru: "Дубликат", en: "Duplicate"} brief: {ru: "x", en: "x"} steps: - text: {ru: "x", en: "x"} choices: - label: {ru: "x", en: "x"} terminal: true success: true result: {ru: "x", en: "x"} `) existing := map[string]bool{"quest.supernova": true} loaded, warnings := LoadWayfarerQuestsFromDir(dir, existing) if len(loaded) != 0 { t.Errorf("квест с уже занятым ключом не должен был загрузиться") } if len(warnings) != 1 { t.Fatalf("ожидалось предупреждение о занятом ключе, получено: %v", warnings) } } func TestLoadWayfarerQuestsFromDirSkipsIncompleteTranslation(t *testing.T) { dir := t.TempDir() writeYAMLFile(t, dir, "incomplete.yaml", ` key: quest.loader_test_incomplete title: {ru: "Есть по-русски", en: ""} brief: {ru: "x", en: "x"} steps: - text: {ru: "x", en: "x"} choices: - label: {ru: "x", en: "x"} terminal: true success: true result: {ru: "x", en: "x"} `) loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil) if len(loaded) != 0 { t.Errorf("квест с неполным переводом не должен был загрузиться") } if len(warnings) == 0 { t.Errorf("ожидалось предупреждение о неполном переводе") } } func TestLoadWayfarerQuestsFromDirIgnoresNonYAMLFiles(t *testing.T) { dir := t.TempDir() writeYAMLFile(t, dir, "readme.txt", "это не квест, просто заметка") loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil) if len(loaded) != 0 || len(warnings) != 0 { t.Errorf("файлы не .yaml/.yml должны молча игнорироваться, получено loaded=%v warnings=%v", loaded, warnings) } } // TestLoadedQuestIsFullyPlayable проверяет, что загруженный из YAML // квест не просто структурно корректен, а реально проходим через // обычный игровой цикл (StartQuest -> ChooseQuestOption -> ...). func TestLoadedQuestIsFullyPlayable(t *testing.T) { dir := t.TempDir() writeYAMLFile(t, dir, "playable.yaml", ` key: quest.loader_test_playable title: {ru: "Играбельный квест", en: "Playable Quest"} brief: {ru: "x", en: "x"} steps: - text: {ru: "Шаг 0", en: "Step 0"} choices: - label: {ru: "Вариант A", en: "Option A"} next_step: 1 - text: {ru: "Шаг 1", en: "Step 1"} choices: - label: {ru: "Завершить успешно", en: "Finish successfully"} terminal: true success: true result: {ru: "Готово", en: "Done"} credits_delta: 250 `) loaded, warnings := LoadWayfarerQuestsFromDir(dir, nil) if len(warnings) != 0 { t.Fatalf("не ожидалось предупреждений: %v", warnings) } if len(loaded) != 1 { t.Fatalf("ожидался 1 квест, получено %d", len(loaded)) } oldDefs := WayfarerQuestDefs WayfarerQuestDefs = append(append([]WayfarerQuestDef{}, oldDefs...), loaded...) defer func() { WayfarerQuestDefs = oldDefs }() s := NewWayfarerShip(WayfarerSystemID{}) creditsBefore := s.Credits if err := s.StartQuest("quest.loader_test_playable"); err != nil { t.Fatalf("неожиданная ошибка запуска квеста: %v", err) } if _, err := s.ChooseQuestOption(0); err != nil { t.Fatalf("неожиданная ошибка выбора на шаге 0: %v", err) } step, ok := s.CurrentQuestStep() if !ok || T(step.TextKey) != "Шаг 1" { t.Fatalf("ожидался переход на шаг 1, получено: %+v (ok=%v)", step, ok) } if _, err := s.ChooseQuestOption(0); err != nil { t.Fatalf("неожиданная ошибка завершающего выбора: %v", err) } if s.ActiveQuest != nil { t.Errorf("квест должен был завершиться") } if !s.CompletedQuests["quest.loader_test_playable"] { t.Errorf("квест должен был отметиться как завершённый успешно") } if s.Credits != creditsBefore+250 { t.Errorf("награда за квест не начислилась: было %v, стало %v", creditsBefore, s.Credits) } }