markovdemo.gno
4.55 Kb · 140 lines
1// Package markovdemo is a small on-chain demo of the Markov-chain text
2// generator provided by the [p/moul/x/daily/markov](/p/moul/x/daily/markov/v0)
3// library.
4//
5// It holds a single package-level [markov.Chain] as the corpus and wires it to
6// the chain: Feed appends words to that persistent chain (a crossing function,
7// since it mutates on-chain state), while Generate and Render walk it with a
8// seed derived from runtime.ChainHeight() — so babbling is a pure, reproducible
9// query that still varies block to block. It contains no Markov logic of its
10// own — build/walk math all lives in the library.
11package markovdemo
12
13import (
14 "chain"
15 "chain/runtime"
16 "strconv"
17 "strings"
18
19 "gno.land/p/moul/x/daily/markov/v0"
20)
21
22// maxGen clamps Generate / Render so output stays bounded.
23const maxGen = 200
24
25// mc is the single on-chain corpus, seeded at deploy time.
26var mc = markov.New()
27
28func init() {
29 // Two seed sentences with heavy two-word-prefix overlap, so a fresh deploy
30 // already babbles something recognizably chain-like.
31 mc.Build("it was the best of times it was the worst of times " +
32 "it was the age of wisdom it was the age of foolishness")
33 mc.Build("i am a free man i am not a number " +
34 "i am the master of my fate i am the captain of my soul")
35}
36
37// seedFor derives a Generate seed from the current chain height so the babble
38// changes block to block yet stays reproducible for anyone querying the same
39// height.
40func seedFor() uint64 {
41 return uint64(runtime.ChainHeight())*2654435761 + 0x9e3779b97f4a7c15
42}
43
44// Feed appends text to the on-chain corpus, extending the Markov chain. Any
45// caller may enrich the corpus — this is the open-membership demo model.
46func Feed(cur realm, text string) string {
47 if strings.TrimSpace(text) == "" {
48 panic("markov: empty text")
49 }
50 added := mc.Build(text)
51 words, prefixes := mc.Stats()
52 chain.Emit("Feed", "words", strconv.Itoa(added), "total", strconv.Itoa(words))
53 return "fed " + strconv.Itoa(added) + " words; corpus now " +
54 strconv.Itoa(words) + " words across " +
55 strconv.Itoa(prefixes) + " prefixes"
56}
57
58// Generate babbles up to nWords of Markov text from the current corpus, seeded
59// deterministically by the chain height. Read-only.
60func Generate(nWords int) string {
61 if nWords <= 0 {
62 nWords = 20
63 }
64 if nWords > maxGen {
65 nWords = maxGen
66 }
67 return strings.Join(mc.Generate(nWords, seedFor()), " ")
68}
69
70// Stats returns (totalWords, prefixCount) for the current corpus.
71func Stats() (int, int) {
72 return mc.Stats()
73}
74
75// parseN pulls a trailing positive integer out of a Render path such as
76// "/40" or "gen/40"; it returns 0 when there is none.
77func parseN(path string) int {
78 p := strings.Trim(path, "/")
79 if i := strings.LastIndex(p, "/"); i >= 0 {
80 p = p[i+1:]
81 }
82 n, err := strconv.Atoi(p)
83 if err != nil || n < 0 {
84 return 0
85 }
86 return n
87}
88
89// Render shows a freshly generated sample plus corpus stats. The path may
90// carry a word count, e.g. Render("/60").
91func Render(path string) string {
92 n := parseN(path)
93 if n == 0 {
94 n = 40
95 }
96 if n > maxGen {
97 n = maxGen
98 }
99
100 words, prefixes := mc.Stats()
101
102 var b strings.Builder
103 b.WriteString("# Markov Babbler\n\n")
104 b.WriteString("A demo of the [`p/moul/x/daily/markov`](/p/moul/x/daily/markov/v0) " +
105 "library — an on-chain port of Go's classic *\"Generating arbitrary text: " +
106 "a Markov chain algorithm\"* codewalk. Every two-word **prefix** maps to " +
107 "the words that followed it; `Generate` walks that chain, picking suffixes " +
108 "with a PRNG seeded by the block height — deterministic, yet different each " +
109 "block.\n\n")
110
111 b.WriteString("## Fresh sample (" + strconv.Itoa(n) + " words, height " +
112 strconv.FormatInt(runtime.ChainHeight(), 10) + ")\n\n")
113 sample := strings.Join(mc.Generate(n, seedFor()), " ")
114 if sample == "" {
115 sample = "_(empty corpus)_"
116 }
117 b.WriteString("> " + sample + "\n\n")
118
119 b.WriteString("## Corpus\n\n")
120 b.WriteString("- **Words:** " + strconv.Itoa(words) + "\n")
121 b.WriteString("- **Distinct prefixes:** " + strconv.Itoa(prefixes) + "\n")
122 b.WriteString("- **Prefix length:** " + strconv.Itoa(markov.PrefixLen) + " words\n\n")
123
124 b.WriteString("## Some prefixes → suffixes\n\n")
125 b.WriteString("| prefix | can be followed by |\n|---|---|\n")
126 shown := 0
127 mc.Iterate(func(k string, suffixes []string) bool {
128 disp := k
129 if disp == " " || disp == "" {
130 disp = "_(start)_"
131 }
132 b.WriteString("| `" + disp + "` | " + strings.Join(suffixes, ", ") + " |\n")
133 shown++
134 return shown >= 12 // cap the table
135 })
136 b.WriteString("\n_Call `Feed(\"your text here\")` to teach it new words, then " +
137 "reload for a new sample._\n")
138
139 return b.String()
140}