diff --git a/00_aMeta/toolz/CYOA/getItWork.txt b/00_aMeta/toolz/CYOA/getItWork.txt new file mode 100644 index 0000000..9dae686 --- /dev/null +++ b/00_aMeta/toolz/CYOA/getItWork.txt @@ -0,0 +1,12 @@ +#nur einmal +go mod init cyoa + +# kompilieren +go build -o cyoa-engine main.go + +# Ausführen & Validieren: +./cyoa-engine -dir ./stories -out export/story.html + +./cyoa-engine -dir ./stories -serve -port 8080 +# Wenn du jetzt in Neovim Dateien abspeicherst oder neue `[[node_id]]`-Knoten anlegst, musst du im +# Browser nur `http://localhost:8080` aufrufen und siehst sofort deine Änderungen inkl. vollständiger Link-Prüfung! diff --git a/00_aMeta/toolz/CYOA/go.mod b/00_aMeta/toolz/CYOA/go.mod new file mode 100644 index 0000000..d13b451 --- /dev/null +++ b/00_aMeta/toolz/CYOA/go.mod @@ -0,0 +1,3 @@ +module cyoa + +go 1.26.5 diff --git a/00_aMeta/toolz/CYOA/main.go b/00_aMeta/toolz/CYOA/main.go new file mode 100644 index 0000000..320f2aa --- /dev/null +++ b/00_aMeta/toolz/CYOA/main.go @@ -0,0 +1,730 @@ +package main + +import ( + "flag" + "fmt" + "html" + "html/template" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" +) + +// Choice beschreibt eine Entscheidungsmöglichkeit zu einem Ziel-Knoten +type Choice struct { + TargetID string `json:"targetId"` + Text string `json:"text"` +} + +// StoryNode repräsentiert einen einzelnen Knotenpunkt in der Geschichte +type StoryNode struct { + ID string `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Choices []Choice `json:"choices"` + IsReboot bool `json:"isReboot"` + IsEnd bool `json:"isEnd"` + FileSource string `json:"fileSource"` +} + +// StoryGraph hält alle Knoten und Analyse-Daten +type StoryGraph struct { + Nodes map[string]*StoryNode `json:"nodes"` + StartNodeID string `json:"startNodeId"` + BrokenLinks []string `json:"brokenLinks"` + OrphanNodes []string `json:"orphanNodes"` + DeadEndNodes []string `json:"deadEndNodes"` +} + +func main() { + dirPtr := flag.String("dir", ".", "Verzeichnis mit AsciiDoc oder Markdown Story-Dateien") + outPtr := flag.String("out", "export/story.html", "Zielpfad für die generierte HTML-Datei") + servePtr := flag.Bool("serve", false, "Startet einen lokalen Preview-Webserver auf -port") + portPtr := flag.Int("port", 8080, "Port für den Preview-Webserver") + flag.Parse() + + fmt.Println("\033[1;36m=== Computer Historyz :: CYOA Engine & Generator ===\033[0m") + + graph := loadStoryGraph(*dirPtr) + validateGraph(graph) + + // Export-Verzeichnis anlegen falls nicht vorhanden + outDir := filepath.Dir(*outPtr) + if err := os.MkdirAll(outDir, 0755); err != nil { + fmt.Printf("\033[1;31m[FEHLER]\033[0m Kann Ordner %s nicht erstellen: %v\n", outDir, err) + os.Exit(1) + } + + // Single-File HTML generieren + err := generateHTML(graph, *outPtr) + if err != nil { + fmt.Printf("\033[1;31m[FEHLER]\033[0m Fehler beim Schreiben der HTML-Datei: %v\n", err) + os.Exit(1) + } + + fmt.Printf("\033[1;32m[ERFOLG]\033[0m Interaktive Story generiert -> \033[1;33m%s\033[0m\n", *outPtr) + + if *servePtr { + startServer(graph, *outPtr, *portPtr, *dirPtr) + } +} + +// loadStoryGraph scannt das Verzeichnis nach .adoc und .md Dateien +func loadStoryGraph(rootDir string) *StoryGraph { + graph := &StoryGraph{ + Nodes: make(map[string]*StoryNode), + } + + // Regex-Muster für Knoten und Links + // AsciiDoc: [[node_id]] gefolgt von === Titel + // Markdown: # node_id oder [[node_id]] + adocNodeRegex := regexp.MustCompile(`\[\[([a-zA-Z0-9_\-]+)\]\]`) + adocChoiceRegex := regexp.MustCompile(`<<([a-zA-Z0-9_\-]+)\s*,\s*([^>]+)>>`) + mdChoiceRegex := regexp.MustCompile(`\[([^\]]+)\]\((?:#)?([a-zA-Z0-9_\-]+)\)`) + headingRegex := regexp.MustCompile(`^(?:={1,4}|#{1,4})\s+(.+)`) + + err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + + ext := strings.ToLower(filepath.Ext(path)) + if ext != ".adoc" && ext != ".md" { + return nil + } + + contentBytes, err := os.ReadFile(path) + if err != nil { + return nil + } + + rawContent := string(contentBytes) + relPath, _ := filepath.Rel(rootDir, path) + + // Dateien anhand von [[node_id]]-Blöcken aufteilen + lines := strings.Split(rawContent, "\n") + var currentNode *StoryNode = nil + var currentBody strings.Builder + + for _, line := range lines { + // Prüfung auf neuen Knoten-Anker [[id]] + if matches := adocNodeRegex.FindStringSubmatch(line); len(matches) > 1 { + if currentNode != nil { + currentNode.Content = cleanBodyText(currentBody.String()) + graph.Nodes[currentNode.ID] = currentNode + currentBody.Reset() + } + + nodeID := matches[1] + currentNode = &StoryNode{ + ID: nodeID, + Title: nodeID, + Choices: make([]Choice, 0), + FileSource: relPath, + } + if graph.StartNodeID == "" || nodeID == "start" || nodeID == "node_start" { + graph.StartNodeID = nodeID + } + continue + } + + if currentNode == nil { + // Fallback: Erste Zeile oder Datei-Header als Standard-Start-Knoten verwenden + if strings.TrimSpace(line) != "" { + baseID := strings.TrimSuffix(filepath.Base(path), ext) + currentNode = &StoryNode{ + ID: baseID, + Title: baseID, + Choices: make([]Choice, 0), + FileSource: relPath, + } + if graph.StartNodeID == "" { + graph.StartNodeID = baseID + } + } else { + continue + } + } + + // Titel extrahieren + if hMatches := headingRegex.FindStringSubmatch(line); len(hMatches) > 1 && currentNode.Title == currentNode.ID { + currentNode.Title = strings.TrimSpace(hMatches[1]) + } + + // AsciiDoc Verzweigung finden: <> + if cMatches := adocChoiceRegex.FindAllStringSubmatch(line, -1); len(cMatches) > 0 { + for _, match := range cMatches { + currentNode.Choices = append(currentNode.Choices, Choice{ + TargetID: match[1], + Text: strings.TrimSpace(match[2]), + }) + } + } + + // Markdown Verzweigung finden: [Optionstext](#ziel_id) + if mMatches := mdChoiceRegex.FindAllStringSubmatch(line, -1); len(mMatches) > 0 { + for _, match := range mMatches { + currentNode.Choices = append(currentNode.Choices, Choice{ + TargetID: match[2], + Text: strings.TrimSpace(match[1]), + }) + } + } + + // Status-Kennzeichnungen erkennen + lowerLine := strings.ToLower(line) + if strings.Contains(lowerLine, "[reboot]") || strings.Contains(lowerLine, "speicherabbild") { + currentNode.IsReboot = true + } + if strings.Contains(lowerLine, "[ende]") || strings.Contains(lowerLine, "[end]") { + currentNode.IsEnd = true + } + + currentBody.WriteString(line + "\n") + } + + if currentNode != nil { + currentNode.Content = cleanBodyText(currentBody.String()) + graph.Nodes[currentNode.ID] = currentNode + } + + return nil + }) + + if err != nil { + fmt.Printf("\033[1;31m[WAR NUNG]\033[0m Fehler beim Durchsuchen der Dateien: %v\n", err) + } + + return graph +} + +// cleanBodyText entfernt Syntax-Rauschen für die HTML-Darstellung +func cleanBodyText(raw string) string { + // Links entfernen/bereinigen + adocChoiceRegex := regexp.MustCompile(`<<[a-zA-Z0-9_\-]+\s*,\s*([^>]+)>>`) + cleaned := adocChoiceRegex.ReplaceAllString(raw, "") + + mdChoiceRegex := regexp.MustCompile(`\[([^\]]+)\]\((?:#)?[a-zA-Z0-9_\-]+\)`) + cleaned = mdChoiceRegex.ReplaceAllString(cleaned, "") + + // HTML-Maskierung + return html.EscapeString(strings.TrimSpace(cleaned)) +} + +// validateGraph prüft Verzweigungen auf Konsistenz +func validateGraph(g *StoryGraph) { + fmt.Printf("Gefundene Knotenpunkte: \033[1;32m%d\033[0m | Start-Knoten: \033[1;33m%s\033[0m\n", len(g.Nodes), g.StartNodeID) + + referenced := make(map[string]bool) + if g.StartNodeID != "" { + referenced[g.StartNodeID] = true + } + + for id, node := range g.Nodes { + // Tote Links prüfen + for _, choice := range node.Choices { + referenced[choice.TargetID] = true + if _, exists := g.Nodes[choice.TargetID]; !exists { + g.BrokenLinks = append(g.BrokenLinks, fmt.Sprintf("%s -> %s (in %s)", id, choice.TargetID, node.FileSource)) + } + } + + // Sackgassen ohne Choices & ohne End-Markierung + if len(node.Choices) == 0 && !node.IsReboot && !node.IsEnd { + g.DeadEndNodes = append(g.DeadEndNodes, id) + } + } + + // Waisen-Knoten ermitteln + for id := range g.Nodes { + if !referenced[id] { + g.OrphanNodes = append(g.OrphanNodes, id) + } + } + + // Konsolen-Bericht + if len(g.BrokenLinks) > 0 { + fmt.Println("\n\033[1;31m[!] DEFECKTE ZIEL-LINKS (Fehlende Knoten):\033[0m") + for _, link := range g.BrokenLinks { + fmt.Printf(" - %s\n", link) + } + } + + if len(g.OrphanNodes) > 0 { + fmt.Println("\n\033[1;33m[?] UNERREICHBARE KNOTEN (Waisen):\033[0m") + for _, orphan := range g.OrphanNodes { + fmt.Printf(" - %s\n", orphan) + } + } + + if len(g.DeadEndNodes) > 0 { + fmt.Println("\n\033[1;35m[*] SACKGASSEN OHNE OPTIONEN:\033[0m") + for _, dead := range g.DeadEndNodes { + fmt.Printf(" - %s\n", dead) + } + } + + if len(g.BrokenLinks) == 0 && len(g.OrphanNodes) == 0 && len(g.DeadEndNodes) == 0 { + fmt.Println("\033[1;32m[✓] Graph-Check bestanden: Alle Verzweigungen sind konsistent!\033[0m") + } +} + +// generateHTML erzeugt das eigenständige HTML5 CRT-Terminal Schreibdeck +func generateHTML(g *StoryGraph, outPath string) error { + tmpl, err := template.New("story").Parse(htmlTemplate) + if err != nil { + return err + } + + f, err := os.Create(outPath) + if err != nil { + return err + } + defer f.Close() + + return tmpl.Execute(f, g) +} + +// startServer führt den Live-Preview-Webserver aus +func startServer(g *StoryGraph, htmlPath string, port int, watchDir string) { + addr := fmt.Sprintf(":%d", port) + fmt.Printf("\n\033[1;34m[SERVER]\033[0m Live Terminal Web-Engine läuft unter: \033[1;32mhttp://localhost:%d\033[0m\n", port) + fmt.Println("Drücke Strg+C zum Beenden.") + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Bei jedem Aufruf Graph neu laden für Hot-Reload Feeling + freshGraph := loadStoryGraph(watchDir) + _ = generateHTML(freshGraph, htmlPath) + http.ServeFile(w, r, htmlPath) + }) + + http.HandleFunc("/api/graph", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + freshGraph := loadStoryGraph(watchDir) + // Einfacher JSON Output + fmt.Fprintf(w, `{"nodes":%d, "start":"%s"}`, len(freshGraph.Nodes), freshGraph.StartNodeID) + }) + + err := http.ListenAndServe(addr, nil) + if err != nil { + fmt.Printf("Server-Fehler: %v\n", err) + } +} + +// Integrierte Retro-CRT Engine Templates (HTML, CSS, JS) +const htmlTemplate = ` + + + + + Computer Historyz :: Interactive Terminal Engine + + + + +
+
[COMPUTER HISTORYZ :: INSTANZ_TERMINAL]
+
+
SPEICHERABBILD: #001
+
HYDRAULIK: 100%
+
KNOTEN: -
+
+
+ +
+
+
System Initialisierung...
+
Verbinde mit Datenstrom...
+
+
+
+ + + + +
+
+ === INTERAKTIVER VORHERSAGE-GRAPH === + +
+ +
+ + + +` \ No newline at end of file diff --git a/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_ausweichmanoever.adoc b/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_ausweichmanoever.adoc new file mode 100644 index 0000000..80e30bc --- /dev/null +++ b/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_ausweichmanoever.adoc @@ -0,0 +1,12 @@ +[[ausweichen]] +=== Knotenpunkt: Ausweichmanöver + +[WARNING] +==== +Systemmeldung: Kinetischer Impuls zu schwach. Magnetischer Fallschirm reagiert träge[cite: 66]. +==== + +Du zündest die Steuerdüsen im vakuumverdichteten Plasma[cite: 68]. Die Kräftewirkung ist enorm[cite: 68]. Dein Speicherabbild verzeichnet eine Parallele zu einem früheren Zyklus – ein Fehler, der dich damals die Primärhülle kostete[cite: 103]. + +* <> +* <> \ No newline at end of file diff --git a/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_start.adoc b/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_start.adoc new file mode 100644 index 0000000..1ddaad6 --- /dev/null +++ b/00_aMeta/toolz/CYOA/stories/cyoa_juniorMiner/node_start.adoc @@ -0,0 +1,11 @@ +[[start]] +=== Sektor 42-A: Die erste Annäherung + +Deine Sensoren erfassen eine massive Dichteschwankung im Trümmerfeld der Oortschen Wolke[cite: 66]. Die Panzerung deines Ernteschiffs vibriert leicht, als der elektromagnetische Trichter Staub einsaugt[cite: 67, 68]. + +Vor dir ragt ein eisenhaltiger Asteroid auf – ein echter "Felsenriese"[cite: 70]. Deine Hydraulik meldet kritische Druckverluste in Trakte 3[cite: 94]. + +* Was entscheidest du, Instanz-404? + +* <> +* <> \ No newline at end of file