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

hangman.gno

7.36 Kb Β· 297 lines
  1// Package hangman is a classic hangman game for gno.land.
  2//
  3// The secret word rotates every ~"day" (a fixed span of blocks) and is chosen
  4// deterministically from a built-in word list using runtime.ChainHeight(). All
  5// players share a single game per day: guesses and the wrong-count are common
  6// state. Guess a letter to reveal every matching position, or lose a life. Six
  7// wrong guesses ends the game.
  8package hangman
  9
 10import (
 11	"chain"
 12	"chain/runtime"
 13	"strings"
 14
 15	"gno.land/p/nt/avl/v0"
 16)
 17
 18// blocksPerDay is the number of blocks that constitutes one "day" / one round.
 19// The secret word is fixed for the whole span, then rotates.
 20const blocksPerDay = 7200
 21
 22// maxWrong is the number of wrong guesses allowed before the game is lost.
 23const maxWrong = 6
 24
 25// words is the built-in dictionary. Kept lowercase, letters only.
 26var words = []string{
 27	"gnoland",
 28	"blockchain",
 29	"realm",
 30	"crossing",
 31	"deterministic",
 32	"contract",
 33	"validator",
 34	"consensus",
 35	"package",
 36	"namespace",
 37	"immutable",
 38	"transaction",
 39	"signature",
 40	"gopher",
 41	"hangman",
 42}
 43
 44// round holds the shared state for a single day's game.
 45type round struct {
 46	word    string   // the secret word for this round (lowercase)
 47	guessed *avl.Tree // letter (string) -> struct{}; ordered set of guesses
 48	wrong   int      // number of wrong guesses so far
 49}
 50
 51// rounds maps a day index (decimal string) to its round. Persisted across calls.
 52var rounds = avl.NewTree()
 53
 54// dayIndex returns the current day index derived from block height.
 55func dayIndex() int64 {
 56	return runtime.ChainHeight() / blocksPerDay
 57}
 58
 59// wordForDay picks a word deterministically from the list for the given day.
 60func wordForDay(day int64) string {
 61	n := int64(len(words))
 62	idx := day % n
 63	if idx < 0 {
 64		idx += n
 65	}
 66	return words[idx]
 67}
 68
 69// dayKey formats a day index as a stable decimal string key.
 70func dayKey(day int64) string {
 71	neg := day < 0
 72	if neg {
 73		day = -day
 74	}
 75	if day == 0 {
 76		return "0"
 77	}
 78	var b []byte
 79	for day > 0 {
 80		b = append([]byte{byte('0' + day%10)}, b...)
 81		day /= 10
 82	}
 83	if neg {
 84		b = append([]byte{'-'}, b...)
 85	}
 86	return string(b)
 87}
 88
 89// current returns (creating if needed) the round for the current day.
 90func current() *round {
 91	day := dayIndex()
 92	key := dayKey(day)
 93	if v := rounds.Get(key); v != nil {
 94		return v.(*round)
 95	}
 96	r := &round{
 97		word:    wordForDay(day),
 98		guessed: avl.NewTree(),
 99		wrong:   0,
100	}
101	rounds.Set(key, r)
102	return r
103}
104
105// normalizeLetter lowercases and validates a single a-z letter, returning it
106// and true on success.
107func normalizeLetter(letter string) (string, bool) {
108	if len(letter) != 1 {
109		return "", false
110	}
111	c := letter[0]
112	if c >= 'A' && c <= 'Z' {
113		c = c - 'A' + 'a'
114	}
115	if c < 'a' || c > 'z' {
116		return "", false
117	}
118	return string(c), true
119}
120
121// isWon reports whether every letter of the word has been guessed.
122func (r *round) isWon() bool {
123	for i := 0; i < len(r.word); i++ {
124		if !r.guessed.Has(string(r.word[i])) {
125			return false
126		}
127	}
128	return true
129}
130
131// isLost reports whether the wrong-count has reached the limit.
132func (r *round) isLost() bool {
133	return r.wrong >= maxWrong
134}
135
136// over reports whether the round is finished either way.
137func (r *round) over() bool {
138	return r.isWon() || r.isLost()
139}
140
141// Guess submits a single letter for the current day's shared game. It reveals
142// matching letters or costs a life. Aborts on bad input or when the game is
143// already over.
144func Guess(cur realm, letter string) {
145	c, ok := normalizeLetter(letter)
146	if !ok {
147		panic("guess must be a single letter a-z")
148	}
149
150	r := current()
151	if r.over() {
152		panic("today's game is already over")
153	}
154	if r.guessed.Has(c) {
155		panic("letter already guessed: " + c)
156	}
157
158	r.guessed.Set(c, struct{}{})
159
160	hit := strings.Contains(r.word, c)
161	if !hit {
162		r.wrong++
163	}
164
165	caller := unsafeCaller(cur)
166	chain.Emit(
167		"Guess",
168		"letter", c,
169		"hit", boolStr(hit),
170		"wrong", itoa(int64(r.wrong)),
171		"by", caller,
172	)
173
174	if r.isWon() {
175		chain.Emit("Win", "day", dayKey(dayIndex()), "word", r.word)
176	} else if r.isLost() {
177		chain.Emit("Lose", "day", dayKey(dayIndex()), "word", r.word)
178	}
179}
180
181// unsafeCaller returns the caller address as a string, preferring the crossing
182// convention when available.
183func unsafeCaller(cur realm) string {
184	if cur.IsCurrent() {
185		return cur.Previous().Address().String()
186	}
187	return ""
188}
189
190// boolStr renders a bool compactly.
191func boolStr(b bool) string {
192	if b {
193		return "true"
194	}
195	return "false"
196}
197
198// itoa renders an int64 as decimal (avl-free tiny helper reused by Render).
199func itoa(n int64) string {
200	return dayKey(n)
201}
202
203// masked returns the word with un-guessed letters shown as underscores.
204func (r *round) masked() string {
205	var sb strings.Builder
206	for i := 0; i < len(r.word); i++ {
207		ch := string(r.word[i])
208		if r.guessed.Has(ch) {
209			sb.WriteString(ch)
210		} else {
211			sb.WriteString("_")
212		}
213		if i != len(r.word)-1 {
214			sb.WriteString(" ")
215		}
216	}
217	return sb.String()
218}
219
220// guessedList returns the guessed letters in sorted order, space-joined.
221func (r *round) guessedList() string {
222	var out []string
223	r.guessed.Iterate("", "", func(key string, _ interface{}) bool {
224		out = append(out, key)
225		return false
226	})
227	return strings.Join(out, " ")
228}
229
230// gallows renders the ASCII stage for the given wrong-count (0..6).
231func gallows(wrong int) string {
232	stages := []string{
233		// 0
234		"  +---+\n  |   |\n      |\n      |\n      |\n      |\n=========",
235		// 1
236		"  +---+\n  |   |\n  O   |\n      |\n      |\n      |\n=========",
237		// 2
238		"  +---+\n  |   |\n  O   |\n  |   |\n      |\n      |\n=========",
239		// 3
240		"  +---+\n  |   |\n  O   |\n /|   |\n      |\n      |\n=========",
241		// 4
242		"  +---+\n  |   |\n  O   |\n /|\\  |\n      |\n      |\n=========",
243		// 5
244		"  +---+\n  |   |\n  O   |\n /|\\  |\n /    |\n      |\n=========",
245		// 6
246		"  +---+\n  |   |\n  O   |\n /|\\  |\n / \\  |\n      |\n=========",
247	}
248	if wrong < 0 {
249		wrong = 0
250	}
251	if wrong >= len(stages) {
252		wrong = len(stages) - 1
253	}
254	return stages[wrong]
255}
256
257// Render shows the current day's game: gallows, masked word, guesses, status.
258func Render(path string) string {
259	r := current()
260
261	var sb strings.Builder
262	sb.WriteString("# πŸͺ’ Hangman\n\n")
263	sb.WriteString("A new secret word every day. Everyone shares the same game β€” guess a letter and see it revealed for all.\n\n")
264	sb.WriteString("Day: `" + dayKey(dayIndex()) + "` Β· Block: `" + itoa(runtime.ChainHeight()) + "`\n\n")
265
266	sb.WriteString("```\n")
267	sb.WriteString(gallows(r.wrong))
268	sb.WriteString("\n```\n\n")
269
270	sb.WriteString("## Word\n\n")
271	sb.WriteString("`" + r.masked() + "`\n\n")
272
273	sb.WriteString("Wrong: **" + itoa(int64(r.wrong)) + " / " + itoa(int64(maxWrong)) + "**  Β·  Lives left: **" + itoa(int64(maxWrong-r.wrong)) + "**\n\n")
274
275	guesses := r.guessedList()
276	if guesses == "" {
277		guesses = "_(none yet)_"
278	}
279	sb.WriteString("Guessed letters: " + guesses + "\n\n")
280
281	sb.WriteString("## Status\n\n")
282	switch {
283	case r.isWon():
284		sb.WriteString("πŸŽ‰ **Solved!** The word was **" + r.word + "**.\n\n")
285	case r.isLost():
286		sb.WriteString("πŸ’€ **Game over.** The word was **" + r.word + "**. Try again next day.\n\n")
287	default:
288		sb.WriteString("Game in progress. Call `Guess(\"a\")` with a letter.\n\n")
289	}
290
291	sb.WriteString("---\n\n")
292	sb.WriteString("### How to play\n\n")
293	sb.WriteString("- `Guess(cur, \"e\")` β€” guess a single letter `a`–`z`.\n")
294	sb.WriteString("- A hit reveals every matching position; a miss costs one of 6 lives.\n")
295	sb.WriteString("- The word rotates every ~" + itoa(int64(blocksPerDay)) + " blocks.\n")
296	return sb.String()
297}