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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

play.gno

9.21 Kb · 358 lines
  1package arena
  2
  3import (
  4	"crypto/sha256"
  5	"encoding/hex"
  6	"strconv"
  7	"strings"
  8	"time"
  9
 10	"gno.land/p/g188vafzutzvlfw6wt7u3s62xtuvm0epmu8ufmfz/gnomepit/engine"
 11)
 12
 13// Accept answers open challenge id as clan, with deck and the round-1 plan in plain
 14// text. When the challenge names an opponent, only that address may accept.
 15func Accept(cur realm, id, clan, deck, plan string) {
 16	rules := engine.ProofOfConcept()
 17	n, m := find(rules, id)
 18	now := time.Now().Unix()
 19	if m.phase != phaseOpen {
 20		panic("wrong phase")
 21	}
 22	if m.expired(now) {
 23		panic("challenge expired")
 24	}
 25	caller := cur.Previous().Address()
 26	if m.opponent != "" && caller != m.opponent {
 27		panic("not the named opponent")
 28	}
 29	c := clanNumber(rules, clan)
 30	if c == 0 {
 31		panic("unknown clan")
 32	}
 33	bits := deckBits(rules, rules.Clans[c-1], deck)
 34	b := planOf(rules, plan)
 35	if !legal(rules, stateOf(rules, m), engine.SideB, bits, b) {
 36		panic("illegal plan")
 37	}
 38
 39	m.acceptor = caller
 40	m.opponent = ""
 41	join(cur.Previous().IsUser(), caller, n)
 42	m.clans[1] = c
 43	m.decks[1] = bits
 44	m.plans[1] = withPlan(m.plans[1], 1, b)
 45	m.phase = phaseReveal
 46	m.deadline = now + moveWindow
 47	saveMatch(rules, n, m)
 48	openRemove(n)
 49	tellBoard(n, "Challenge "+strconv.Itoa(n)+" was taken.")
 50	tell("Turn", m.challenger, caller, n, clan+" accepted your challenge. Your move within 24 hours.")
 51}
 52
 53// Reveal opens the challenger's sealed plan for the round being played, and on the
 54// first reveal the sealed deck, then resolves the round. A match that goes on needs
 55// nextSeal and nextBox for the challenger's next plan; one that ends takes neither.
 56func Reveal(cur realm, id, deck, deckSalt, plan, planSalt, nextSeal, nextBox string) {
 57	rules := engine.ProofOfConcept()
 58	n, m := find(rules, id)
 59	now := time.Now().Unix()
 60	caller := cur.Previous().Address()
 61	guard(cur.Previous().IsUser(), caller, m, n)
 62	m.checkMove(caller, phaseReveal, m.challenger, now)
 63
 64	s := stateOf(rules, m)
 65	first := s.Round == 1
 66	bits := m.decks[0]
 67	if first {
 68		if deck == "" {
 69			panic("deck required")
 70		}
 71		checkSeal(m.deckSeal, deckSalt, deck)
 72		bits = deckBits(rules, rules.Clans[m.clans[0]-1], deck)
 73	} else if deck != "" || deckSalt != "" {
 74		panic("deck already revealed")
 75	}
 76	checkSeal(m.seal, planSalt, plan)
 77	a := planOf(rules, plan)
 78	if !legal(rules, s, engine.SideA, bits, a) {
 79		panic("illegal plan")
 80	}
 81	o := rules.Resolve(s, a, planAt(m.plans[1], s.Round))
 82	var box string
 83	if o.Result == engine.Ongoing {
 84		if nextSeal == "" {
 85			panic("next seal required")
 86		}
 87		if !validSeal(nextSeal) {
 88			panic("invalid seal")
 89		}
 90		box = boxBytes(nextBox)
 91	} else if nextSeal != "" || nextBox != "" {
 92		panic("unexpected next seal")
 93	}
 94
 95	if first {
 96		m.decks[0] = bits
 97		m.deckSeal = ""
 98	}
 99	m.plans[0] = withPlan(m.plans[0], s.Round, a)
100	for side, g := range o.State.Gnomes {
101		m.hp[s.Round-1][side] = g.HP
102		m.carried[side] = engine.Vitals{Vulnerable: g.Vulnerable, Strength: g.Strength}
103	}
104	m.seal, m.box = nextSeal, box
105	if o.Result != engine.Ongoing {
106		m.end(resultName(o.Result), reasonOf(o.State))
107		saveMatch(rules, n, m)
108		rate(rules, m)
109		tellEnded(m, n, caller)
110		return
111	}
112	m.phase = phasePlay
113	m.deadline = now + moveWindow
114	saveMatch(rules, n, m)
115	hp := m.hp[s.Round-1]
116	tell("Turn", m.acceptor, caller, n, "Round "+strconv.Itoa(s.Round)+" is over: you "+strconv.Itoa(hp[1])+
117		" HP, them "+strconv.Itoa(hp[0])+". Your move within 24 hours.")
118}
119
120// Play records the acceptor's plan for the round being played.
121func Play(cur realm, id, plan string) {
122	rules := engine.ProofOfConcept()
123	n, m := find(rules, id)
124	now := time.Now().Unix()
125	caller := cur.Previous().Address()
126	guard(cur.Previous().IsUser(), caller, m, n)
127	m.checkMove(caller, phasePlay, m.acceptor, now)
128	s := stateOf(rules, m)
129	b := planOf(rules, plan)
130	if !legal(rules, s, engine.SideB, m.decks[1], b) {
131		panic("illegal plan")
132	}
133	m.plans[1] = withPlan(m.plans[1], s.Round, b)
134	m.phase = phaseReveal
135	m.deadline = now + moveWindow
136	saveMatch(rules, n, m)
137	tell("Turn", m.challenger, caller, n, clanName(rules, m.clans[1])+" played round "+strconv.Itoa(s.Round)+". Your move within 24 hours.")
138}
139
140// Resign withdraws the caller's open challenge, expired or not, deleting it, or concedes
141// a running match.
142func Resign(cur realm, id string) {
143	rules := engine.ProofOfConcept()
144	n, m := find(rules, id)
145	caller := cur.Previous().Address()
146	guard(cur.Previous().IsUser(), caller, m, n)
147	switch m.phase {
148	case phaseOpen:
149		if caller != m.challenger {
150			panic("not your challenge")
151		}
152		openRemove(n)
153		forget(n, m.challenger)
154		tellBoard(n, "Challenge "+strconv.Itoa(n)+" was withdrawn.")
155	case phaseReveal, phasePlay:
156		switch caller {
157		case m.challenger:
158			m.end("b", "resign")
159		case m.acceptor:
160			m.end("a", "resign")
161		default:
162			panic("not a player")
163		}
164		saveMatch(rules, n, m)
165		rate(rules, m)
166		tellEnded(m, n, caller)
167	default:
168		panic("wrong phase")
169	}
170}
171
172// ClaimForfeit ends a running match whose deadline has passed: the player who had to
173// move loses. Any wallet may claim it; a session may claim only a match its wallet plays.
174func ClaimForfeit(cur realm, id string) {
175	rules := engine.ProofOfConcept()
176	n, m := find(rules, id)
177	caller := cur.Previous().Address()
178	guard(cur.Previous().IsUser(), caller, m, n)
179	var result string
180	switch m.phase {
181	case phaseReveal:
182		result = "b"
183	case phasePlay:
184		result = "a"
185	default:
186		panic("wrong phase")
187	}
188	if time.Now().Unix() <= m.deadline {
189		panic("deadline not passed")
190	}
191	m.end(result, "forfeit")
192	saveMatch(rules, n, m)
193	rate(rules, m)
194	tellEnded(m, n, caller)
195}
196
197func find(rules *engine.Ruleset, id string) (int, record) {
198	n, m, ok := lookup(rules, id)
199	if !ok {
200		panic("unknown match")
201	}
202	return n, m
203}
204
205// lookup returns the match id names, false when there is none. Only an id's own decimal
206// text names it, never "01" or "+1".
207func lookup(rules *engine.Ruleset, id string) (int, record, bool) {
208	if len(id) == 0 || len(id) > idDigits {
209		return 0, record{}, false
210	}
211	n, err := strconv.Atoi(id)
212	if err != nil || strconv.Itoa(n) != id {
213		return 0, record{}, false
214	}
215	m, ok := loadMatch(rules, n)
216	return n, m, ok
217}
218
219func (m record) checkMove(caller address, phase string, mover address, now int64) {
220	if m.phase != phase {
221		panic("wrong phase")
222	}
223	if caller != mover {
224		panic("not your move")
225	}
226	if now > m.deadline {
227		panic("deadline passed")
228	}
229}
230
231func (m *record) end(result, reason string) {
232	m.phase = phaseEnded
233	m.result = result
234	m.reason = reason
235	m.deckSeal, m.seal, m.box = "", "", ""
236}
237
238// stateOf rebuilds the engine state the next round of m starts from. A round is resolved
239// once the challenger's plan for it is stored; the specials of resolved rounds are spent.
240func stateOf(rules *engine.Ruleset, m record) engine.State {
241	resolved := plansMade(m.plans[0])
242	s := rules.Start()
243	s.Round = resolved + 1
244	for side := range s.Gnomes {
245		g := &s.Gnomes[side]
246		if resolved > 0 {
247			g.HP = m.hp[resolved-1][side]
248		}
249		g.Vulnerable = m.carried[side].Vulnerable
250		g.Strength = m.carried[side].Strength
251		for round := 1; round <= resolved; round++ {
252			for _, id := range planAt(m.plans[side], round) {
253				if rules.Cards[id].Special {
254					g.Spent |= uint64(1) << uint(id)
255				}
256			}
257		}
258	}
259	return s
260}
261
262// checkSeal panics unless seal is the sha256 of salt's bytes followed by text.
263func checkSeal(seal, salt, text string) {
264	if !validSeal(salt) {
265		panic("invalid salt")
266	}
267	b, _ := hex.DecodeString(salt)
268	sum := sha256.Sum256(append(b, text...))
269	if hex.EncodeToString(sum[:]) != seal {
270		panic("seal mismatch")
271	}
272}
273
274// clanNumber is clan's index in rules plus one, as a record stores it, or 0 when no clan
275// has that name.
276func clanNumber(rules *engine.Ruleset, clan string) int {
277	for i, c := range rules.Clans {
278		if c.Name == clan {
279			return i + 1
280		}
281	}
282	return 0
283}
284
285// clanName is the name of the clan a record stores as n, "" for none.
286func clanName(rules *engine.Ruleset, n int) string {
287	if n == 0 {
288		return ""
289	}
290	return rules.Clans[n-1].Name
291}
292
293// planOf maps comma-separated card names to the engine's ids.
294func planOf(rules *engine.Ruleset, text string) engine.Plan {
295	p, ok := rules.PlanOf(strings.Split(text, ",")...)
296	if !ok {
297		panic("unknown card")
298	}
299	return p
300}
301
302// deckBits returns deck as the engine's bit set, panicking unless it is legal for c. A
303// card named twice is illegal, so a legal deck text holds exactly the deck's names.
304func deckBits(rules *engine.Ruleset, c engine.Clan, deck string) uint64 {
305	var bits uint64
306	for _, id := range planOf(rules, deck) {
307		bit := uint64(1) << uint(id)
308		if bits&bit != 0 {
309			panic("illegal deck")
310		}
311		bits |= bit
312	}
313	if !rules.LegalDeck(c, bits) {
314		panic("illegal deck")
315	}
316	return bits
317}
318
319func legal(rules *engine.Ruleset, s engine.State, side engine.Side, deck uint64, p engine.Plan) bool {
320	for _, q := range rules.LegalPlans(s, side, deck) {
321		if samePlan(p, q) {
322			return true
323		}
324	}
325	return false
326}
327
328func samePlan(p, q engine.Plan) bool {
329	if len(p) != len(q) {
330		return false
331	}
332	for i := range p {
333		if p[i] != q[i] {
334			return false
335		}
336	}
337	return true
338}
339
340func resultName(r engine.Result) string {
341	switch r {
342	case engine.AWins:
343		return "a"
344	case engine.BWins:
345		return "b"
346	case engine.Draw:
347		return "draw"
348	}
349	return ""
350}
351
352// reasonOf says why a finished match ended: a gnome at 0 HP or less is a knockout.
353func reasonOf(s engine.State) string {
354	if s.Gnomes[0].HP <= 0 || s.Gnomes[1].HP <= 0 {
355		return "knockout"
356	}
357	return "hp"
358}