storytelling

This commit is contained in:
2026-07-10 01:30:09 +07:00
parent 377dd26e3e
commit 99f0e50f9a
6 changed files with 937 additions and 1 deletions
+2
View File
@@ -26,3 +26,5 @@ go.work
go.sum go.sum
.env .env
.VSCodeCounter/
+1
View File
@@ -1,5 +1,6 @@
{ {
"cSpell.words": [ "cSpell.words": [
"Аркхема",
"pgxpool", "pgxpool",
"timestamppb" "timestamppb"
] ]
+9 -1
View File
@@ -4,12 +4,20 @@ go 1.25.0
require ( require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0
github.com/stretchr/testify v1.11.1
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8
google.golang.org/grpc v1.64.0 google.golang.org/grpc v1.64.0
google.golang.org/protobuf v1.34.1 google.golang.org/protobuf v1.34.1
) )
require github.com/vearutop/statigz v1.4.0 // indirect require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.15.0 // indirect
github.com/vearutop/statigz v1.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require ( require (
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
@@ -0,0 +1,76 @@
package storytelling
type IStory interface {
GetStory(scenario *Story, codes []string) *Story
}
// История - это набор точек
type Story struct {
// Список точек
Places []*Place `json:"places"`
}
// Точка - что-то куда можно сходить
// Это может быть место - аптека, остановка, площадь...
// Это может быть персонаж - пострадавший, продавец, полицейский...
// Это может быть описание действия игрока - ответ на вопрос, реакция на выбитую дверь...
type Place struct {
// Код - то что вбивает игрок
Code string `json:"code"`
// Название места, имя персонажа, заданный игроком вопрос...
Name string `json:"name"`
// Текст - получаемая текстовая информация
Text string `json:"text"`
// Картинка - получаемая визуальная информация
Image string `json:"image"`
// Признак скрытой карточки - некоторые места изначально не доступны
Hidden bool `json:"hidden"`
// Приложения - получаемые улики
Applications []*Application `json:"applications,omitempty"`
// Двери к другим точкам - действия или диалоги, открывающие другие точки
Doors []*Door `json:"doors"`
// Получаемые ключи - ключи используются для получения доступа к скрытым дверям
Keys []*Key `json:"keys"`
}
// Приложение - улика
// Это может быть оружие, отпечатки, фотография...
type Application struct {
// Название улики
Name string `json:"name"`
// Картинка
Image string `json:"image"`
}
// Дверь - действие или диалог
type Door struct {
// Код
Code string `json:"code"`
// Название - заданный вопрос, проделанное действие...
Name string `json:"name"`
// Некоторые двери изначально не доступны
// Ключи требуемые для получения доступа к скрытым дверям
// Если указаны ключи то дверь считается скрытой
Keys []*Key `json:"keys"`
}
// Аналог карт допуска в "Тайны Аркхема"
type Key struct {
// Название
Name string `json:"name"`
}
+172
View File
@@ -0,0 +1,172 @@
package storytelling
type story struct{}
func NewStory() IStory {
return &story{}
}
func (s *story) GetStory(
scenario *Story,
codes []string,
) *Story {
story := &Story{}
keys := map[string]struct{}{}
codesWithUseActions := map[string]struct{}{}
codesHidden := map[string]struct{}{}
for i, code := range codes {
var prevPlace *Place
if i > 0 {
prevPlace, _ = s.findPlace(scenario, codes[i-1])
}
currentPlace, _ := s.findPlace(scenario, codes[i])
var nextPlace *Place
if i < len(codes)-1 {
nextPlace, _ = s.findPlace(scenario, codes[i+1])
}
// Если следующая точка это открытие двери в текущей точке, то текущая точка считается использованной для действия
if nextPlace != nil && currentPlace != nil {
openDoor := false
for _, door := range currentPlace.Doors {
if door.Code == nextPlace.Code {
codesWithUseActions[currentPlace.Code] = struct{}{}
openDoor = true
break
}
}
if openDoor {
// Запрещаем смотреть не выбранные действия
for _, door := range currentPlace.Doors {
if door.Code != nextPlace.Code {
codesHidden[door.Code] = struct{}{}
}
}
}
}
// Собираем ключи
if currentPlace != nil {
for _, key := range currentPlace.Keys {
keys[key.Name] = struct{}{}
}
}
place, ok := s.findResultPlace(scenario, code, prevPlace, codesHidden)
if !ok {
story.Places = append(story.Places, s.buildNotFoundPlace(code))
continue
}
story.Places = append(
story.Places,
s.mapPlace(place, nextPlace, codesWithUseActions, keys),
)
}
return story
}
func (s *story) findPlace(
scenario *Story,
code string,
) (*Place, bool) {
for _, place := range scenario.Places {
if place.Code == code {
return place, true
}
}
return nil, false
}
func (s *story) findResultPlace(
scenario *Story,
code string,
prevPlace *Place,
codesHidden map[string]struct{},
) (*Place, bool) {
if _, ok := codesHidden[code]; ok {
return nil, false
}
open := false
if prevPlace != nil {
for _, door := range prevPlace.Doors {
if door.Code == code {
open = true
break
}
}
}
for _, place := range scenario.Places {
if place.Code == code {
if place.Hidden && !open {
return nil, false
}
return place, true
}
}
return nil, false
}
func (s *story) buildNotFoundPlace(code string) *Place {
return &Place{
Code: code,
Name: "Не найдено",
Text: "Такой точки не существует.",
}
}
func (s *story) mapPlace(
place *Place,
nextPlace *Place,
codesWithUseActions map[string]struct{},
keys map[string]struct{},
) *Place {
var applications []*Application
if len(place.Applications) > 0 {
applications = make([]*Application, 0, len(place.Applications))
for _, application := range place.Applications {
applications = append(
applications,
&Application{
Name: application.Name,
Image: application.Image,
},
)
}
}
var doors []*Door
_, useAction := codesWithUseActions[place.Code]
if len(place.Doors) > 0 && !useAction {
doors = make([]*Door, 0, len(place.Doors))
for _, door := range place.Doors {
if nextPlace != nil {
if door.Code != nextPlace.Code {
continue
}
}
hasKeys := true
for _, doorKey := range door.Keys {
if _, ok := keys[doorKey.Name]; !ok {
hasKeys = false
}
}
if !hasKeys {
continue
}
doors = append(
doors,
&Door{
Code: door.Code,
Name: door.Name,
},
)
}
}
if len(doors) == 0 {
doors = nil
}
return &Place{
Code: place.Code,
Name: place.Name,
Text: place.Text,
Image: place.Image,
Applications: applications,
Doors: doors,
}
}
+677
View File
@@ -0,0 +1,677 @@
package storytelling
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_story_GetStory(t *testing.T) {
tests := []struct {
name string
scenario *Story
codes []string
want *Story
}{
{
name: "Попытка получить несуществующую точку",
scenario: &Story{},
codes: []string{"Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Не найдено",
Text: "Такой точки не существует.",
},
},
},
},
{
name: "Получение точки",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
codes: []string{"Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
},
{
name: "Получение 2 точек",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки Ы",
Text: "Текст точки Ы.",
},
{
Code: "Д",
Name: "Название точки Д",
Text: "Текст точки Д.",
},
},
},
codes: []string{"Ы", "Д"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки Ы",
Text: "Текст точки Ы.",
},
{
Code: "Д",
Name: "Название точки Д",
Text: "Текст точки Д.",
},
},
},
},
{
name: "Получение точки с картинкой",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Image: "image.png",
},
},
},
codes: []string{"Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Image: "image.png",
},
},
},
},
{
name: "Попытка получить скрытую точку",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Hidden: true,
},
},
},
codes: []string{"Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Не найдено",
Text: "Такой точки не существует.",
},
},
},
},
{
name: "Получение точки с уликой",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Applications: []*Application{
{
Name: "Название улики",
Image: "image.png",
},
},
},
},
},
codes: []string{"Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Applications: []*Application{
{
Name: "Название улики",
Image: "image.png",
},
},
},
},
},
},
{
name: "Получение точки с действием",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
},
},
},
},
},
codes: []string{"Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
},
},
},
},
},
},
{
name: "Не открываем скрытую точку действием",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
},
},
},
{
Code: "Д",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
codes: []string{"Ы", "Д"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Д",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
},
{
name: "Открываем скрытую точку действием",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
},
},
},
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
Hidden: true,
},
},
},
codes: []string{"Ы", "Ы-1"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
},
{
name: "Не открываем скрытую точку действием, а потом возвращаемся",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
},
},
},
{
Code: "Д",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
codes: []string{"Ы", "Д", "Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Д",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
},
},
},
},
},
},
{
name: "Открываем скрытую точку действием, а потом возвращаемся",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
},
},
},
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
Hidden: true,
},
},
},
codes: []string{"Ы", "Ы-1", "Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
},
{
name: "Получение точки со скрытым действием",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
Keys: []*Key{
{
Name: "ключ",
},
},
},
},
},
},
},
codes: []string{"Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
},
{
name: "Получение точки с ключом",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Keys: []*Key{
{
Name: "ключ",
},
},
},
},
},
codes: []string{"Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
},
{
name: "Получение ключа и открытие скрытого действия",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Keys: []*Key{
{
Name: "ключ",
},
},
},
{
Code: "Д",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
Keys: []*Key{
{
Name: "ключ",
},
},
},
},
},
},
},
codes: []string{"Ы", "Д"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Д",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
},
},
},
},
},
},
{
name: "После выбора одного из действий остальные не доступны",
scenario: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Ы-1",
Name: "Действие 1",
},
{
Code: "Ы-2",
Name: "Действие 1",
},
},
},
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
Hidden: true,
},
{
Code: "Ы-2",
Name: "Название точки",
Text: "Текст точки.",
Hidden: true,
},
},
},
codes: []string{"Ы", "Ы-1", "Ы-2", "Ы", "Ы-2"},
want: &Story{
Places: []*Place{
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы-2",
Name: "Не найдено",
Text: "Такой точки не существует.",
},
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы-2",
Name: "Не найдено",
Text: "Такой точки не существует.",
},
},
},
},
{
name: "Если ключей не хватает то скрытые двери не видим",
scenario: &Story{
Places: []*Place{
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
Keys: []*Key{
{
Name: "ключ 1",
},
},
},
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Д",
Name: "Действие",
Keys: []*Key{
{
Name: "ключ 1",
},
{
Name: "ключ 2",
},
},
},
},
},
},
},
codes: []string{"Ы-1", "Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
},
},
},
},
{
name: "Собираем ключи и видим скрытые двери",
scenario: &Story{
Places: []*Place{
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
Keys: []*Key{
{
Name: "ключ 1",
},
},
},
{
Code: "Ы-2",
Name: "Название точки",
Text: "Текст точки.",
Keys: []*Key{
{
Name: "ключ 2",
},
},
},
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Д",
Name: "Действие",
Keys: []*Key{
{
Name: "ключ 1",
},
{
Name: "ключ 2",
},
},
},
},
},
},
},
codes: []string{"Ы-1", "Ы-2", "Ы"},
want: &Story{
Places: []*Place{
{
Code: "Ы-1",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы-2",
Name: "Название точки",
Text: "Текст точки.",
},
{
Code: "Ы",
Name: "Название точки",
Text: "Текст точки.",
Doors: []*Door{
{
Code: "Д",
Name: "Действие",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewStory().GetStory(tt.scenario, tt.codes)
assert.Equal(t, got, tt.want)
})
}
}