test the graph

This commit is contained in:
2026-07-27 18:40:05 +02:00
parent ba3de2b63b
commit ea7852256f
11 changed files with 185 additions and 64 deletions
+12
View File
@@ -0,0 +1,12 @@
FROM debian:trixie-slim
# Asciidoctor installieren und apt-Cache sofort wieder leeren, um das Image klein zu halten
RUN apt-get update && \
apt-get install -y --no-install-recommends asciidoctor && \
rm -rf /var/lib/apt/lists/*
# ENTRYPOINT als Echo Box:
# -s = embedded (kein html/body wraper)
# -o - = output auf stdout
# - = read von stdin
ENTRYPOINT ["asciidoctor", "-s", "-o", "-", "-"]
+4
View File
@@ -10,3 +10,7 @@ go build -o cyoa-engine main.go
./cyoa-engine -dir ./stories -serve -port 8080 ./cyoa-engine -dir ./stories -serve -port 8080
# Wenn du jetzt in Neovim Dateien abspeicherst oder neue `[[node_id]]`-Knoten anlegst, musst du im # 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! # Browser nur `http://localhost:8080` aufrufen und siehst sofort deine Änderungen inkl. vollständiger Link-Prüfung!
# baue das docker image
docker build -t asciidoctor-echo -f Dockerfile.echobox .
+34 -37
View File
@@ -7,6 +7,7 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
@@ -78,8 +79,6 @@ func loadStoryGraph(rootDir string) *StoryGraph {
} }
// Regex-Muster für Knoten und Links // 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_\-]+)\]\]`) adocNodeRegex := regexp.MustCompile(`\[\[([a-zA-Z0-9_\-]+)\]\]`)
adocChoiceRegex := regexp.MustCompile(`<<([a-zA-Z0-9_\-]+)\s*,\s*([^>]+)>>`) adocChoiceRegex := regexp.MustCompile(`<<([a-zA-Z0-9_\-]+)\s*,\s*([^>]+)>>`)
mdChoiceRegex := regexp.MustCompile(`\[([^\]]+)\]\((?:#)?([a-zA-Z0-9_\-]+)\)`) mdChoiceRegex := regexp.MustCompile(`\[([^\]]+)\]\((?:#)?([a-zA-Z0-9_\-]+)\)`)
@@ -112,7 +111,7 @@ func loadStoryGraph(rootDir string) *StoryGraph {
// Prüfung auf neuen Knoten-Anker [[id]] // Prüfung auf neuen Knoten-Anker [[id]]
if matches := adocNodeRegex.FindStringSubmatch(line); len(matches) > 1 { if matches := adocNodeRegex.FindStringSubmatch(line); len(matches) > 1 {
if currentNode != nil { if currentNode != nil {
currentNode.Content = cleanBodyText(currentBody.String()) currentNode.Content = renderWithEchoBox(currentBody.String())
graph.Nodes[currentNode.ID] = currentNode graph.Nodes[currentNode.ID] = currentNode
currentBody.Reset() currentBody.Reset()
} }
@@ -186,7 +185,7 @@ func loadStoryGraph(rootDir string) *StoryGraph {
} }
if currentNode != nil { if currentNode != nil {
currentNode.Content = cleanBodyText(currentBody.String()) currentNode.Content = renderWithEchoBox(currentBody.String())
graph.Nodes[currentNode.ID] = currentNode graph.Nodes[currentNode.ID] = currentNode
} }
@@ -194,23 +193,34 @@ func loadStoryGraph(rootDir string) *StoryGraph {
}) })
if err != nil { if err != nil {
fmt.Printf("\033[1;31m[WAR NUNG]\033[0m Fehler beim Durchsuchen der Dateien: %v\n", err) fmt.Printf("\033[1;31m[WARNUNG]\033[0m Fehler beim Durchsuchen der Dateien: %v\n", err)
} }
return graph return graph
} }
// cleanBodyText entfernt Syntax-Rauschen für die HTML-Darstellung // renderWithEchoBox entfernt Syntax-Rauschen und nutzt den Debian 13 Docker Container als HTML Echo Box
func cleanBodyText(raw string) string { func renderWithEchoBox(raw string) string {
// Links entfernen/bereinigen fmt.Println("render with docker")
// Links entfernen (Diese sollen als Buttons, nicht als Text-Wirrwarr auftauchen)
adocChoiceRegex := regexp.MustCompile(`<<[a-zA-Z0-9_\-]+\s*,\s*([^>]+)>>`) adocChoiceRegex := regexp.MustCompile(`<<[a-zA-Z0-9_\-]+\s*,\s*([^>]+)>>`)
cleaned := adocChoiceRegex.ReplaceAllString(raw, "") cleaned := adocChoiceRegex.ReplaceAllString(raw, "")
mdChoiceRegex := regexp.MustCompile(`\[([^\]]+)\]\((?:#)?[a-zA-Z0-9_\-]+\)`) mdChoiceRegex := regexp.MustCompile(`\[([^\]]+)\]\((?:#)?[a-zA-Z0-9_\-]+\)`)
cleaned = mdChoiceRegex.ReplaceAllString(cleaned, "") cleaned = mdChoiceRegex.ReplaceAllString(cleaned, "")
// HTML-Maskierung // Docker Echo Box aufrufen (Debian 13 Asciidoctor)
cmd := exec.Command("docker", "run", "--rm", "-i", "asciidoctor-echo")
cmd.Stdin = strings.NewReader(strings.TrimSpace(cleaned))
out, err := cmd.Output()
if err != nil {
// Fallback bei Docker-Fehler (z.B. wenn der Daemon nicht läuft)
fmt.Printf("\033[1;31m[DOCKER FEHLER]\033[0m Konnte Echo Box nicht nutzen. Fallback auf RAW Text.\n")
return html.EscapeString(strings.TrimSpace(cleaned)) return html.EscapeString(strings.TrimSpace(cleaned))
}
return string(out)
} }
// validateGraph prüft Verzweigungen auf Konsistenz // validateGraph prüft Verzweigungen auf Konsistenz
@@ -246,7 +256,7 @@ func validateGraph(g *StoryGraph) {
// Konsolen-Bericht // Konsolen-Bericht
if len(g.BrokenLinks) > 0 { if len(g.BrokenLinks) > 0 {
fmt.Println("\n\033[1;31m[!] DEFECKTE ZIEL-LINKS (Fehlende Knoten):\033[0m") fmt.Println("\n\033[1;31m[!] DEFEKTE ZIEL-LINKS (Fehlende Knoten):\033[0m")
for _, link := range g.BrokenLinks { for _, link := range g.BrokenLinks {
fmt.Printf(" - %s\n", link) fmt.Printf(" - %s\n", link)
} }
@@ -300,13 +310,6 @@ func startServer(g *StoryGraph, htmlPath string, port int, watchDir string) {
http.ServeFile(w, r, 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) err := http.ListenAndServe(addr, nil)
if err != nil { if err != nil {
fmt.Printf("Server-Fehler: %v\n", err) fmt.Printf("Server-Fehler: %v\n", err)
@@ -360,7 +363,6 @@ const htmlTemplate = `<!DOCTYPE html>
pointer-events: none; pointer-events: none;
} }
/* Window Maker Header */
header { header {
background: #111; background: #111;
border-bottom: 2px solid var(--term-green); border-bottom: 2px solid var(--term-green);
@@ -389,7 +391,6 @@ const htmlTemplate = `<!DOCTYPE html>
background: rgba(0, 20, 10, 0.5); background: rgba(0, 20, 10, 0.5);
} }
/* Hauptbereich */
main { main {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -403,7 +404,6 @@ const htmlTemplate = `<!DOCTYPE html>
z-index: 1; z-index: 1;
} }
/* Story Terminal Content */
.node-card { .node-card {
border: 1px solid var(--term-dim); border: 1px solid var(--term-dim);
background: rgba(5, 15, 10, 0.85); background: rgba(5, 15, 10, 0.85);
@@ -424,11 +424,19 @@ const htmlTemplate = `<!DOCTYPE html>
.node-body { .node-body {
line-height: 1.6; line-height: 1.6;
font-size: 1rem; font-size: 1rem;
white-space: pre-wrap;
margin-bottom: 20px; margin-bottom: 20px;
} }
/* Choices List */ /* AsciiDoc Formatierungen im Terminal Look */
.node-body p { margin-bottom: 15px; }
.node-body ul, .node-body ol { margin-bottom: 15px; margin-left: 20px; }
.node-body li { margin-bottom: 5px; }
.node-body strong, .node-body b { color: var(--term-amber); }
.node-body em, .node-body i { color: var(--term-cyan); font-style: italic;}
.node-body code { background: rgba(0, 255, 102, 0.1); padding: 2px 5px; border-radius: 3px; font-family: var(--font-stack); }
.node-body h1, .node-body h2, .node-body h3, .node-body h4 { color: var(--term-cyan); margin: 20px 0 10px 0; border-bottom: 1px dashed var(--term-dim); padding-bottom: 5px; }
.node-body blockquote { border-left: 3px solid var(--term-dim); padding-left: 10px; margin-bottom: 15px; font-style: italic; opacity: 0.8; }
.choices-container { .choices-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -471,7 +479,6 @@ const htmlTemplate = `<!DOCTYPE html>
color: var(--term-green); color: var(--term-green);
} }
/* Controls & Graph Modal Toggle */
footer { footer {
border-top: 1px solid var(--term-dim); border-top: 1px solid var(--term-dim);
padding: 10px 20px; padding: 10px 20px;
@@ -496,7 +503,6 @@ const htmlTemplate = `<!DOCTYPE html>
color: var(--term-cyan); color: var(--term-cyan);
} }
/* Graph Canvas View */
#graph-modal { #graph-modal {
display: none; display: none;
position: fixed; position: fixed;
@@ -541,17 +547,15 @@ const htmlTemplate = `<!DOCTYPE html>
</div> </div>
</footer> </footer>
<!-- Visual Graph Overlay -->
<div id="graph-modal"> <div id="graph-modal">
<div style="display:flex; justify-size:space-between; margin-bottom:100px;"> <div style="display:flex; justify-size:space-between; margin-bottom:10px;">
<span style="color:var(--term-cyan)">=== INTERAKTIVER VORHERSAGE-GRAPH ===</span> <span style="color:var(--term-cyan)">=== INTERAKTIVER VORHERSAGE-GRAPH ===</span>
<button class="btn-small" onclick="toggleGraphView()">[X] SCHLIESSEN</button> <button class="btn-small" style="margin-left: auto;" onclick="toggleGraphView()">[X] SCHLIESSEN</button>
</div> </div>
<canvas id="graph-canvas"></canvas> <canvas id="graph-canvas"></canvas>
</div> </div>
<script> <script>
// Story Graph Injection aus Go Backend
const storyData = {{.}}; const storyData = {{.}};
let currentNodeID = storyData.startNodeId || Object.keys(storyData.nodes)[0]; let currentNodeID = storyData.startNodeId || Object.keys(storyData.nodes)[0];
@@ -559,7 +563,6 @@ const htmlTemplate = `<!DOCTYPE html>
let hullIntegrity = 100; let hullIntegrity = 100;
let history = []; let history = [];
// Synthesizer Audio API (Retro-Beeps)
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playBeep(freq = 440, type = 'square', duration = 0.05) { function playBeep(freq = 440, type = 'square', duration = 0.05) {
try { try {
@@ -590,7 +593,8 @@ const htmlTemplate = `<!DOCTYPE html>
document.getElementById('stat-node-id').innerText = node.id; document.getElementById('stat-node-id').innerText = node.id;
document.getElementById('node-title').innerText = "=== " + node.title + " ==="; document.getElementById('node-title').innerText = "=== " + node.title + " ===";
document.getElementById('node-body').innerText = node.content; // WICHTIG: Nutzt jetzt gerendertes HTML statt purem Text
document.getElementById('node-body').innerHTML = node.content;
const choicesBox = document.getElementById('choices-box'); const choicesBox = document.getElementById('choices-box');
choicesBox.innerHTML = ''; choicesBox.innerHTML = '';
@@ -622,7 +626,6 @@ const htmlTemplate = `<!DOCTYPE html>
choicesBox.appendChild(btn); choicesBox.appendChild(btn);
} }
// Automatischer Scroll nach oben
document.getElementById('terminal-viewport').scrollTop = 0; document.getElementById('terminal-viewport').scrollTop = 0;
} }
@@ -632,7 +635,6 @@ const htmlTemplate = `<!DOCTYPE html>
renderNode(storyData.startNodeId); renderNode(storyData.startNodeId);
} }
// Keyboard Shortcuts
window.addEventListener('keydown', (e) => { window.addEventListener('keydown', (e) => {
if (e.key >= '1' && e.key <= '9') { if (e.key >= '1' && e.key <= '9') {
const idx = parseInt(e.key) - 1; const idx = parseInt(e.key) - 1;
@@ -647,7 +649,6 @@ const htmlTemplate = `<!DOCTYPE html>
} }
}); });
// Visueller Canvas Graph
function toggleGraphView() { function toggleGraphView() {
const modal = document.getElementById('graph-modal'); const modal = document.getElementById('graph-modal');
if (modal.style.display === 'flex') { if (modal.style.display === 'flex') {
@@ -675,7 +676,6 @@ const htmlTemplate = `<!DOCTYPE html>
const positions = {}; const positions = {};
// Knoten im Kreis anordnen
keys.forEach((key, idx) => { keys.forEach((key, idx) => {
const angle = (idx / total) * 2 * Math.PI; const angle = (idx / total) * 2 * Math.PI;
const x = centerX + radius * Math.cos(angle); const x = centerX + radius * Math.cos(angle);
@@ -683,7 +683,6 @@ const htmlTemplate = `<!DOCTYPE html>
positions[key] = { x, y }; positions[key] = { x, y };
}); });
// Kanten zeichnen
ctx.strokeStyle = 'rgba(51, 255, 102, 0.3)'; ctx.strokeStyle = 'rgba(51, 255, 102, 0.3)';
ctx.lineWidth = 1; ctx.lineWidth = 1;
@@ -701,7 +700,6 @@ const htmlTemplate = `<!DOCTYPE html>
} }
}); });
// Knotenpunkte zeichnen
keys.forEach(key => { keys.forEach(key => {
const pos = positions[key]; const pos = positions[key];
ctx.beginPath(); ctx.beginPath();
@@ -721,7 +719,6 @@ const htmlTemplate = `<!DOCTYPE html>
}); });
} }
// Starten
window.onload = () => { window.onload = () => {
renderNode(currentNodeID); renderNode(currentNodeID);
}; };
@@ -1,16 +1,20 @@
[[funk]] [[funk]]
=== Konfiguration: Komunijation wieder herstellen # Konfiguration: Komunijation wieder herstellen
[WARNING] #### Systemmeldung
==== ```
Systemmeldung: OK . : OK .
==== ```
## Kontext
Daten fliessen in die Komunikation Buffer, erste Trägerfrequenzen wurden gefunden, automatisches nachführen aktiv. + Daten fliessen in die Komunikation Buffer, erste Trägerfrequenzen wurden gefunden, automatisches nachführen aktiv. +
Aber dann war es das auch schon, keine Kontakt zur Flotte keine Updates. Da draussen ist etwa und es hat wohl mit den selben Schwirigkeiten zu kämpfen. Aber dann war es das auch schon, keine Kontakt zur Flotte keine Updates. Da draussen ist etwa und es hat wohl mit den selben Schwirigkeiten zu kämpfen.
* <<steuerung, [01] die Steuerung mit simpler sensorig.>>
* <<waffen, [02] die Waffen auf automatik schalten.>>
* <<sensoren, [03] alle Sensoren zur anayse.>>
* <<start, [04] zum anfang zurück.>>
## Was werden wir ausführen?
<<steuerung, [01] die Steuerung mit simpler sensorig.>>
<<waffen, [02] die Waffen auf automatik schalten.>>
<<sensoren, [03] alle Sensoren zur anayse.>>
<<start, [04] zum anfang zurück.>>
@@ -0,0 +1,20 @@
[[sensoren]]
# Konfiguration: Sensoren zur anayse
#### Systemmeldung
```
: sensor daten angeschlossen
```
## Kontext
### Was werden wir ausführen?
<<funk, [01] die Komunikation auf dem Haupt Kanal evtl Updates der Flotte.>>
<<waffen, [02] die Waffen auf automatik schalten.>>
<<steuerung, [03] die Steuerung mit simpler sensorig.>>
<<start, [04] zum anfang zurück.>>
@@ -1,5 +1,5 @@
[[start]] [[start]]
## Sektor 42-A: Die erste Annäherung # Sektor 42-A: Die erste Annäherung
Welche Konfiguration ist die richtige, wir wissen einfach noch nicht genug über die beanstandetetn Fehler. + Welche Konfiguration ist die richtige, wir wissen einfach noch nicht genug über die beanstandetetn Fehler. +
Wie nicht nur in der Computer Technik üblicher werden wir das gute alte Ausschlussverfahren anwenden. Folgendende Optionen bieten sich uns als nächste zum ausschliessen an. Wie nicht nur in der Computer Technik üblicher werden wir das gute alte Ausschlussverfahren anwenden. Folgendende Optionen bieten sich uns als nächste zum ausschliessen an.
@@ -1,16 +1,21 @@
[[steuerung]] [[steuerung]]
=== Konfiguration: Steuerung wieder herstellen # Konfiguration: Steuerung wieder herstellen
[WARNING] #### Systemmeldung
==== ```
Systemmeldung: einige Variablen konnten nicht verarbeitet werden : einige Variablen konnten nicht verarbeitet werden
==== ```
## Kontext
Die verarbeitung hat sehr lange gedauert, kein gutes Zeichen. Die Meldung sagt aber eben auch nichts genaueres. + Die verarbeitung hat sehr lange gedauert, kein gutes Zeichen. Die Meldung sagt aber eben auch nichts genaueres. +
Hier braucht es noch einige versuche um an die teile zu gelangen die nicht zur Eingabe passen. Erst eine Block ausschluss Verharen und dann Zeilenweise bis der Punkt gefunden wurde wo es bricht. Hier braucht es noch einige versuche um an die teile zu gelangen die nicht zur Eingabe passen. Erst eine Block ausschluss Verharen und dann Zeilenweise bis der Punkt gefunden wurde wo es bricht.
* <<funk, [01] die Komunikation auf dem Haupt Kanal evtl Updates der Flotte.>> ### Was werden wir ausführen?
* <<waffen, [02] die Waffen auf automatik schalten.>>
* <<sensoren, [03] alle Sensoren zur anayse.>> <<steuerungoben, [01] steuer config oben.>>
* <<start, [04] zum anfang zurück.>> <<steuerungmitte, [02] steuer config mitte.>>
<<steuerungunten, [03] steuer config unten.>>
<<start, [04] zum anfang zurück.>>
@@ -0,0 +1,20 @@
[[steuerungmitte]]
# Konfiguration: Steuerung wieder herstellen
#### Systemmeldung
```
: einige Variablen konnten nicht verarbeitet werden
```
## Kontext
Mitte
### Was werden wir ausführen?
<<steuerungoben, [01] steuer config oben.>>
<<steuerungmitte, [02] steuer config mitte.>>
<<steuerungunten, [03] steuer config unten.>>
<<start, [04] zum anfang zurück.>>
@@ -0,0 +1,20 @@
[[steuerungoben]]
# Konfiguration: Steuerung wieder herstellen
#### Systemmeldung
```
: einige Variablen konnten nicht verarbeitet werden
```
## Kontext
Oben
### Was werden wir ausführen?
<<steuerungoben, [01] steuer config oben.>>
<<steuerungmitte, [02] steuer config mitte.>>
<<steuerungunten, [03] steuer config unten.>>
<<start, [04] zum anfang zurück.>>
@@ -0,0 +1,20 @@
[[steuerungunten]]
# Konfiguration: Steuerung wieder herstellen
#### Systemmeldung
```
: einige Variablen konnten nicht verarbeitet werden
```
## Kontext
Mitte
### Was werden wir ausführen?
<<steuerungoben, [01] steuer config oben.>>
<<steuerungmitte, [02] steuer config mitte.>>
<<steuerungunten, [03] steuer config unten.>>
<<start, [04] zum anfang zurück.>>
@@ -0,0 +1,19 @@
[[waffen]]
# Konfiguration: Waffen auf automatik schalten
#### Systemmeldung
```
: energy low, stand by on low power
```
## Kontext
### Was werden wir ausführen?
<<steuerung, [01] die Steuerung mit simpler sensorig.>>
<<funk, [02] die Komunikation auf dem Haupt Kanal evtl Updates der Flotte.>>
<<sensoren, [03] alle Sensoren zur anayse.>>
<<start, [04] zum anfang zurück.>>