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.

shots.gno

11.57 Kb · 322 lines
  1// Package shots is a pay-to-play penalty shootout against the chain.
  2//
  3// A creator opens a game by choosing its keeper and ball constants and
  4// seeding the pot. A challenger joins an open game by paying its entry fee
  5// into the pot, then plays a shootout: in every round they take a kick
  6// (TakeShot) and then keep against the chain's kick (SubmitKeeperPlan). Five
  7// kicks each, finishing early when one side cannot be caught, then sudden
  8// death. Win, and the whole pot is paid out; lose, and the entry fee stays in
  9// the pot for the next challenger.
 10//
 11// Every kick is played out in the transaction that makes it: the player's
 12// choices are completed by 32 bytes derived from the block's timestamp (the
 13// chain's move and the mishit), and the outcome is penalty.SimulateShot of the
 14// ten inputs, recorded so anyone can re-run it. See impl/v1 for what that
 15// entropy does and does not guarantee.
 16//
 17// This realm's path and entry points are permanent. The rules engine is an
 18// implementation realm behind an upgradeable proxy
 19// (gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0); the games and their pots live here
 20// and survive every upgrade.
 21package shots
 22
 23import (
 24	"chain/runtime/unsafe"
 25	"strconv"
 26	"strings"
 27
 28	"gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/penalty/v0"
 29	"gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/shootout/v0"
 30	"gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0"
 31)
 32
 33// Impl is the rules engine. Declared here so every version agrees on one
 34// type identity. The methods are non-crossing: this realm threads its own cur
 35// in, so an implementation reaching Store is seen as this realm.
 36//
 37// The caller's address and the coins received are established by the entry
 38// points below and passed in; an implementation cannot be invoked with forged
 39// ones from elsewhere, because without this realm's live cur it cannot reach
 40// the state at all.
 41type Impl interface {
 42	CreateGame(_ int, rlm realm, creator address, deposit, entryFee, timeoutBlocks int64, config string) uint64
 43	JoinGame(_ int, rlm realm, player address, gameID uint64, paid int64)
 44	TopUp(_ int, rlm realm, from address, gameID uint64, paid int64)
 45	TakeShot(_ int, rlm realm, player address, gameID uint64, shot penalty.ShotInput) shootout.Kick
 46	SubmitKeeperPlan(_ int, rlm realm, player address, gameID uint64, plan penalty.KeeperPlan) shootout.Kick
 47	Forfeit(_ int, rlm realm, caller address, gameID uint64)
 48	CancelGame(_ int, rlm realm, caller address, gameID uint64)
 49
 50	// SimulateShot is the pure outcome function: six taker inputs (in shot,
 51	// mishit seed included) and four keeper inputs, under cfg.
 52	SimulateShot(shot penalty.ShotParams, plan penalty.KeeperPlan, cfg penalty.Config) penalty.Result
 53
 54	// Describe names the implementation for Render.
 55	Describe() string
 56}
 57
 58// Denom is the coin pots are held in.
 59const Denom = "ugnot"
 60
 61// Admin may accept, roll back or freeze an implementation. The value is the
 62// standard test1 account so the tests run unedited; set your own address
 63// before deploying (TransferAuthority moves it afterwards).
 64const Admin = address("g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm")
 65
 66var (
 67	proxy    *upgradeable.Proxy
 68	selfPath string
 69)
 70
 71func init(cur realm) {
 72	selfPath = cur.PkgPath()
 73	proxy = upgradeable.New(upgradeable.NewAddrAuthority(Admin))
 74}
 75
 76// ---------------------------------------------------------------- playing
 77
 78// CreateGame opens a game. Send the initial pot with the call (ugnot).
 79// config overrides the default constants as "key=value,..." ("" for the
 80// defaults; see penalty.ParseConfig). timeoutBlocks 0 picks the default.
 81// Returns the game id.
 82func CreateGame(cur realm, entryFee, timeoutBlocks int64, config string) uint64 {
 83	paid := receivePayment(0, cur)
 84	return live().CreateGame(0, cur, cur.Previous().Address(), paid, entryFee, timeoutBlocks, config)
 85}
 86
 87// JoinGame starts a shootout in an open game. Send exactly its entry fee.
 88func JoinGame(cur realm, gameID uint64) {
 89	paid := receivePayment(0, cur)
 90	live().JoinGame(0, cur, cur.Previous().Address(), gameID, paid)
 91}
 92
 93// TopUp adds the coins sent to a game's pot.
 94func TopUp(cur realm, gameID uint64) {
 95	paid := receivePayment(0, cur)
 96	live().TopUp(0, cur, cur.Previous().Address(), gameID, paid)
 97}
 98
 99// TakeShot plays the player's kick for the current round against the chain's
100// keeper: aim point on the goal plane (mm), contact offset on the ball
101// (milli-radii) and power (per-mille). Returns the kick as JSON.
102func TakeShot(cur realm, gameID uint64, aimX, aimY, contactX, contactY, power int64) string {
103	assertFreeDirectCall(0, cur)
104	shot := penalty.ShotInput{AimX: aimX, AimY: aimY, ContactX: contactX, ContactY: contactY, Power: power}
105	return kickJSON(live().TakeShot(0, cur, cur.Previous().Address(), gameID, shot))
106}
107
108// SubmitKeeperPlan plays the player's keeper plan against the chain's kick
109// for the current round: four uint16 (see penalty.DecodePlan). Returns the
110// kick as JSON.
111func SubmitKeeperPlan(cur realm, gameID uint64, direction, speed, jump, reaction uint16) string {
112	assertFreeDirectCall(0, cur)
113	plan := penalty.KeeperPlan{Direction: direction, Speed: speed, Jump: jump, Reaction: reaction}
114	return kickJSON(live().SubmitKeeperPlan(0, cur, cur.Previous().Address(), gameID, plan))
115}
116
117// Forfeit ends the current shootout as a loss: the player may resign at any
118// time, anyone else once the player has been idle for the game's timeout.
119func Forfeit(cur realm, gameID uint64) {
120	assertFreeDirectCall(0, cur)
121	live().Forfeit(0, cur, cur.Previous().Address(), gameID)
122}
123
124// CancelGame refunds the pot to the creator of a game nobody has played yet.
125func CancelGame(cur realm, gameID uint64) {
126	assertFreeDirectCall(0, cur)
127	live().CancelGame(0, cur, cur.Previous().Address(), gameID)
128}
129
130// ---------------------------------------------------------------- reading
131
132// SimulateShot runs the live outcome function on ten inputs, without touching
133// any state: the taker's aim, contact, power and 48-hex-char mishit seed, and
134// the keeper's four uint16, under config ("" for the defaults). Returns JSON.
135func SimulateShot(aimX, aimY, contactX, contactY, power int64, mishitHex string,
136	direction, speed, jump, reaction uint16, config string) string {
137	cfg, err := penalty.ParseConfig(config)
138	if err != nil {
139		panic(err)
140	}
141	m, ok := penalty.ParseMishitHex(mishitHex)
142	if !ok {
143		panic("shots: mishit seed must be 48 hex characters")
144	}
145	shot := penalty.ShotInput{AimX: aimX, AimY: aimY, ContactX: contactX, ContactY: contactY, Power: power}.WithMishit(m)
146	plan := penalty.KeeperPlan{Direction: direction, Speed: speed, Jump: jump, Reaction: reaction}
147	return resultJSON(live().SimulateShot(shot, plan, cfg))
148}
149
150// GameJSON returns game id as JSON ("" if it does not exist).
151func GameJSON(gameID uint64) string {
152	g, ok := getGame(gameID)
153	if !ok {
154		return ""
155	}
156	return gameJSON(g)
157}
158
159// SessionJSON returns session no of a game, kicks included, as JSON.
160func SessionJSON(gameID uint64, no int) string {
161	s, ok := getSession(gameID, no)
162	if !ok {
163		return ""
164	}
165	return sessionJSON(s)
166}
167
168// ListGames returns up to limit games as a JSON array, newest first,
169// optionally filtered by status ("open", "playing", "won", "cancelled"; ""
170// for all).
171func ListGames(status string, offset, limit int) string {
172	if limit <= 0 || limit > 100 {
173		limit = 100
174	}
175	out := []string{}
176	skipped := 0
177	games.ReverseIterate("", "", func(_ string, v any) bool {
178		g := v.(*shootout.Game)
179		if status != "" && g.Status.String() != status {
180			return false
181		}
182		if skipped < offset {
183			skipped++
184			return false
185		}
186		out = append(out, gameJSON(*g))
187		return len(out) >= limit
188	})
189	return "[" + strings.Join(out, ",") + "]"
190}
191
192// VerifySession re-runs every recorded kick of a shootout through the live
193// outcome function and reports whether all of them reproduce, so anyone can
194// check a result from the ten recorded inputs alone.
195func VerifySession(gameID uint64, no int) bool {
196	g, ok := getGame(gameID)
197	if !ok {
198		return false
199	}
200	s, ok := getSession(gameID, no)
201	if !ok {
202		return false
203	}
204	im := live()
205	for _, k := range s.Kicks {
206		if im.SimulateShot(k.Shot, k.Plan, g.Config) != k.Result {
207			return false
208		}
209	}
210	return true
211}
212
213// GameCount returns how many games have been created.
214func GameCount() uint64 { return lastID }
215
216// ---------------------------------------------------------------- guards
217
218// receivePayment returns the ugnot sent with a direct user call. The coin
219// envelope is credited to this realm only when this realm is the message's
220// target, so a payment from any other call shape is refused rather than
221// trusted.
222func receivePayment(_ int, rlm realm) int64 {
223	assertDirect(0, rlm)
224	var amount int64
225	for _, c := range unsafe.OriginSend() {
226		if c.Denom != Denom {
227			panic("shots: only " + Denom + " is accepted, got " + c.Denom)
228		}
229		amount += c.Amount
230	}
231	return amount
232}
233
234// assertFreeDirectCall requires a direct user call that sends no coins.
235func assertFreeDirectCall(_ int, rlm realm) {
236	assertDirect(0, rlm)
237	if len(unsafe.OriginSend()) > 0 {
238		panic("shots: this call does not accept coins")
239	}
240}
241
242func assertDirect(_ int, rlm realm) {
243	if !rlm.IsCurrent() || !rlm.Previous().IsUserCall() {
244		panic("shots: call this directly from an account (no run scripts or other realms)")
245	}
246}
247
248// ---------------------------------------------------------------- upgrades
249
250// Register records the calling realm as a candidate implementation; an
251// implementation calls it from its own init. Nothing serves until Accept.
252func Register(cur realm, impl Impl) { proxy.Propose(0, cur, impl) }
253
254// Accept makes the candidate at pkgPath live. Authority only.
255func Accept(cur realm, pkgPath string) { proxy.Accept(0, cur, pkgPath) }
256
257// Withdraw drops a candidate. Authority only, or the candidate itself.
258func Withdraw(cur realm, pkgPath string) { proxy.Withdraw(0, cur, pkgPath) }
259
260// Rollback restores the previous implementation. Authority only.
261func Rollback(cur realm) { proxy.Rollback(0, cur) }
262
263// Forget drops the rollback history. Authority only.
264func Forget(cur realm) { proxy.Forget(0, cur) }
265
266// Freeze ends upgradeability permanently. Authority only.
267func Freeze(cur realm) { proxy.Freeze(0, cur) }
268
269// AddExtension lets another realm act as this one. Authority only.
270func AddExtension(cur realm, pkgPath string) { proxy.AddExtension(0, cur, pkgPath) }
271
272// DropExtension revokes one. Authority only.
273func DropExtension(cur realm, pkgPath string) { proxy.DropExtension(0, cur, pkgPath) }
274
275// TransferAuthority hands the power to upgrade to another address.
276func TransferAuthority(cur realm, newOwner address) {
277	proxy.TransferAuthority(0, cur, upgradeable.NewAddrAuthority(newOwner))
278}
279
280// TransferAuthorityToRealms hands it to governance realms (comma-separated).
281func TransferAuthorityToRealms(cur realm, csvPaths string) {
282	paths := []string{}
283	for _, p := range strings.Split(csvPaths, ",") {
284		paths = append(paths, strings.TrimSpace(p))
285	}
286	proxy.TransferAuthority(0, cur, upgradeable.NewRealmAuthority(paths...))
287}
288
289func LivePath() string     { return proxy.LivePath() }
290func Frozen() bool         { return proxy.Frozen() }
291func Authority() string    { return proxy.Authority().String() }
292func Extensions() []string { return proxy.Extensions() }
293
294func PendingPaths() []string {
295	out := []string{}
296	for _, r := range proxy.Pending() {
297		out = append(out, r.PkgPath())
298	}
299	return out
300}
301
302func HistoryPaths() []string {
303	out := []string{}
304	for _, r := range proxy.History() {
305		out = append(out, r.PkgPath())
306	}
307	return out
308}
309
310func live() Impl {
311	v, ok := proxy.TryImpl()
312	if !ok {
313		panic("shots: no implementation has been accepted yet")
314	}
315	im, ok := v.(Impl)
316	if !ok {
317		panic("shots: live implementation does not satisfy Impl")
318	}
319	return im
320}
321
322func itoa(n int64) string { return strconv.FormatInt(n, 10) }