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

eightball.gno

3.34 Kb · 117 lines
  1// Package eightball is a Magic 8-Ball realm: ask a yes/no question and the
  2// block height decides your fate. Deterministic (no real randomness on-chain).
  3package eightball
  4
  5import (
  6	"chain"
  7	"chain/runtime"
  8	"chain/runtime/unsafe"
  9	"strconv"
 10	"strings"
 11)
 12
 13// The 20 classic Magic 8-Ball answers.
 14var answers = []string{
 15	"It is certain.", "It is decidedly so.", "Without a doubt.",
 16	"Yes definitely.", "You may rely on it.", "As I see it, yes.",
 17	"Most likely.", "Outlook good.", "Yes.", "Signs point to yes.",
 18	"Reply hazy, try again.", "Ask again later.", "Better not tell you now.",
 19	"Cannot predict now.", "Concentrate and ask again.",
 20	"Don't count on it.", "My reply is no.", "My sources say no.",
 21	"Outlook not so good.", "Very doubtful.",
 22}
 23
 24type shake struct {
 25	asker    string
 26	question string
 27	answer   string
 28	height   int64
 29}
 30
 31var history []shake
 32
 33// answerIndex maps (block height, prior shakes) to an answer slot. Pure and
 34// deterministic so it can be tested without touching realm state.
 35func answerIndex(height int64, n int) int {
 36	i := (height + int64(n)) % int64(len(answers))
 37	if i < 0 {
 38		i += int64(len(answers))
 39	}
 40	return int(i)
 41}
 42
 43// Ask poses a question to the Magic 8-Ball and returns its answer. Crossing
 44// function: it mutates persistent state, so it takes `cur realm` (gno 0.9).
 45func Ask(cur realm, question string) string {
 46	question = strings.TrimSpace(question)
 47	if question == "" {
 48		panic("question must not be empty")
 49	}
 50	if len(question) > 200 {
 51		panic("question too long (max 200 chars)")
 52	}
 53	h := runtime.ChainHeight()
 54	ans := answers[answerIndex(h, len(history))]
 55	history = append(history, shake{
 56		asker:    unsafe.PreviousRealm().Address().String(),
 57		question: question,
 58		answer:   ans,
 59		height:   h,
 60	})
 61	chain.Emit("Shaken", "answer", ans, "height", strconv.FormatInt(h, 10))
 62	return ans
 63}
 64
 65// Asked returns how many questions have been asked. Read-only.
 66func Asked() int { return len(history) }
 67
 68// Render is the gnoweb Markdown view.
 69func Render(path string) string {
 70	var b strings.Builder
 71	b.WriteString("# 🎱 Magic 8-Ball\n\n")
 72	b.WriteString("> ⚠️ Experimental, auto-generated with no human supervision (MCP test corpus). ")
 73	b.WriteString("Not audited. See [r/moul/x/daily](https://github.com/moul/gno-contracts/blob/main/r/moul/x/daily/README.md).\n\n")
 74	b.WriteString("Ask a yes/no question — the block height decides your fate.\n\n")
 75	b.WriteString("**Questions asked:** ")
 76	b.WriteString(strconv.Itoa(len(history)))
 77	b.WriteString("\n\n")
 78
 79	if len(history) == 0 {
 80		b.WriteString("_No questions yet. Call `Ask(\"will it rain tomorrow?\")`._\n")
 81		return b.String()
 82	}
 83
 84	last := history[len(history)-1]
 85	b.WriteString("Last shake: 🎱 _")
 86	b.WriteString(last.answer)
 87	b.WriteString("_\n\n| # | asker | question | answer | height |\n")
 88	b.WriteString("|---|---|---|---|---|\n")
 89
 90	start := len(history) - 1
 91	end := start - 14
 92	if end < 0 {
 93		end = 0
 94	}
 95	for i := start; i >= end; i-- {
 96		e := history[i]
 97		b.WriteString("| ")
 98		b.WriteString(strconv.Itoa(i + 1))
 99		b.WriteString(" | ")
100		b.WriteString(shortAddr(e.asker))
101		b.WriteString(" | ")
102		b.WriteString(e.question)
103		b.WriteString(" | ")
104		b.WriteString(e.answer)
105		b.WriteString(" | ")
106		b.WriteString(strconv.FormatInt(e.height, 10))
107		b.WriteString(" |\n")
108	}
109	return b.String()
110}
111
112func shortAddr(a string) string {
113	if len(a) <= 13 {
114		return a
115	}
116	return a[:8] + "…" + a[len(a)-4:]
117}