lobby.gno
5.91 Kb · 159 lines
1package gnoslinger
2
3import (
4 "strconv"
5 "strings"
6 "time"
7
8 "gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/engine/v0"
9 "gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/hex/v0"
10 profiles "gno.land/r/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/profiles/v0"
11)
12
13// CreateGame opens a showdown of seats players (engine.MinPlayers to
14// engine.MaxPlayers) with the caller in seat 0, playing the showdown's
15// first character (engine.Setups: the marshal), standing in startHex, one
16// of the hexes assigned to that character, facing f (0=N 1=NE 2=SE 3=S
17// 4=SW 5=NW); it returns the game id. The other seats take the next
18// characters in the order the players join. maxTurns is the game length,
19// from MinMaxTurns to MaxMaxTurns; timeoutMinutes is the time each turn
20// gives the players, past which anyone may claim it with ClaimTimeout, 0
21// meaning DefaultTimeout (2 days). The game stays open until its seats fill
22// up or its creator cancels it: the turn clock starts with the last join.
23// The ugnot sent with the call are the showdown's stake, which every
24// joiner must send too; none, a free showdown (stakes.gno).
25func CreateGame(cur realm, seats int, maxTurns int, timeoutMinutes int, startHex string, facing int) string {
26 caller := cur.Previous().Address()
27 stake := sentStake(cur)
28 if seats < engine.MinPlayers || seats > engine.MaxPlayers {
29 panic("gnoslinger: seats must be between " + strconv.Itoa(engine.MinPlayers) + " and " + strconv.Itoa(engine.MaxPlayers))
30 }
31 if maxTurns < MinMaxTurns || maxTurns > MaxMaxTurns {
32 panic("gnoslinger: maxTurns must be between " + strconv.Itoa(MinMaxTurns) + " and " + strconv.Itoa(MaxMaxTurns))
33 }
34 timeout := DefaultTimeout
35 if timeoutMinutes != 0 {
36 timeout = time.Duration(timeoutMinutes) * time.Minute
37 }
38 if timeout < MinTimeout || timeout > MaxTimeout {
39 panic("gnoslinger: timeoutMinutes must be between " + strconv.Itoa(int(MinTimeout/time.Minute)) + " and " + strconv.Itoa(int(MaxTimeout/time.Minute)))
40 }
41 charKey := engine.SeatChar(0)
42 assertSetUp(charKey, startHex, facing)
43 id := gameSeq.Next().String()
44 g := &Game{
45 ID: id,
46 Players: make([]Player, seats),
47 State: engine.NewState(seats),
48 Turn: 1,
49 MaxTurns: maxTurns,
50 Timeout: timeout,
51 Phase: PhaseWaiting,
52 Missed: make([]int, seats),
53 Winner: -1,
54 VP: make([]int, seats),
55 Stake: stake,
56 Pot: stake,
57 CreatedAt: time.Now(),
58 }
59 g.clearTurn()
60 g.Players[0] = Player{Addr: caller, Char: charKey}
61 g.State.SetUp(0, startHex, hex.Dir(facing))
62 openGames.Set(id, g)
63 index(caller, id)
64 profiles.Ensure(cross(cur), caller)
65 save(g)
66 emit("GameCreated", "id", id, "creator", caller.String(), "char", charKey, "seats", strconv.Itoa(seats),
67 "maxTurns", strconv.Itoa(maxTurns), "timeoutMinutes", strconv.Itoa(int(timeout/time.Minute)), "hex", startHex, "facing", strconv.Itoa(facing),
68 "stake", strconv.FormatInt(stake, 10))
69 return id
70}
71
72// JoinGame seats the caller in the first free seat of a waiting game,
73// playing that seat's character (the kid, then the reb, then the yankee),
74// standing in startHex, one of that character's hexes, facing f; the game
75// starts collecting the commits of turn 1 once its last seat is taken.
76// The call must send the showdown's stake, no more, no less.
77func JoinGame(cur realm, id string, startHex string, facing int) {
78 caller := cur.Previous().Address()
79 stake := sentStake(cur)
80 g := getGame(id)
81 if g.Phase != PhaseWaiting {
82 panic("gnoslinger: game is not open")
83 }
84 switch g.seat(caller) {
85 case 0:
86 panic("gnoslinger: you cannot join your own game")
87 case -1:
88 default:
89 panic("gnoslinger: you are already in this game")
90 }
91 seat := g.freeSeat()
92 if seat < 0 {
93 panic("gnoslinger: every seat is taken")
94 }
95 charKey := engine.SeatChar(seat)
96 assertSetUp(charKey, startHex, facing)
97 if stake != g.Stake {
98 panic("gnoslinger: this showdown's stake is " + strconv.FormatInt(g.Stake, 10) + Denom + ", sent " + strconv.FormatInt(stake, 10) + Denom)
99 }
100 g.Pot += stake
101 g.Players[seat] = Player{Addr: caller, Char: charKey}
102 g.State.SetUp(seat, startHex, hex.Dir(facing))
103 index(caller, id)
104 if g.freeSeat() < 0 {
105 // The game starts: it counts as played for every seat.
106 g.Phase = PhasePlanning
107 g.startTurn()
108 g.notifyPlanDue()
109 openGames.Remove(id)
110 for _, p := range g.Players {
111 profiles.AddGamePlayed(cross(cur), p.Addr)
112 }
113 } else {
114 profiles.Ensure(cross(cur), caller)
115 }
116 save(g)
117 emit("PlayerJoined", "id", id, "player", caller.String(), "seat", strconv.Itoa(seat), "char", charKey, "hex", startHex, "facing", strconv.Itoa(facing))
118}
119
120// assertSetUp refuses a character that is not in the showdown, a hex it
121// may not set up in, or no such facing.
122func assertSetUp(charKey, startHex string, facing int) {
123 setup := engine.SetupOf(charKey)
124 if setup == nil {
125 panic("gnoslinger: " + charKey + " is not in this showdown: play " + strings.Join(engine.ShowdownChars(), " or "))
126 }
127 if !engine.CanSetUp(charKey, startHex) {
128 panic("gnoslinger: " + charKey + " sets up in " + strings.Join(setup.Hexes, ", ") + ", not in " + startHex)
129 }
130 if !hex.Dir(facing).Valid() {
131 panic("gnoslinger: facing must be between 0 (N) and 5 (NW)")
132 }
133}
134
135// CancelGame withdraws a game whose seats are not all taken yet. Creator
136// only. The game is deleted, not kept as finished, so the creator gets back
137// the storage deposit CreateGame paid; the players who had joined are
138// unindexed with it, and every stake paid goes back.
139func CancelGame(cur realm, id string) {
140 assertNoSend()
141 caller := cur.Previous().Address()
142 g := getGame(id)
143 if g.Phase != PhaseWaiting {
144 panic("gnoslinger: only a game waiting for players can be cancelled")
145 }
146 if g.Players[0].Addr != caller {
147 panic("gnoslinger: only the creator can cancel the game")
148 }
149 g.refundStakes(cur)
150 openGames.Remove(id)
151 games.Remove(id)
152 for _, p := range g.Players {
153 if p.Addr != "" {
154 unindex(p.Addr, id)
155 }
156 }
157 emit("GameEnded", "id", id, "reason", "cancelled", "winner", "")
158 g.notifyCancelled(caller)
159}