Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}