← Back to Realms

Realm

gno.land/r/moul/x/daily/connect4/v0

Overview

Realm Path
gno.land/r/moul/x/daily/connect4/v0
Exported Functions
3
State Entries
8
Source Files
2
Total Package Entries
20

Exported Functions

3 exported functions

State

8 state entries

Source Code

FILES
connect4.gno
go
1package connect4
2
3import (
4	"chain"
5	"chain/runtime/unsafe"
6	"errors"
7	"strconv"
8	"strings"
9
10	"gno.land/p/nt/avl/v0"
11)
12
13const (
14	cols = 7
15	rows = 6
16)
17
18// cell values
19const (
20	empty = 0
21	red   = 1 // 🔴 game creator (caller of NewGame)
22	yel   = 2 // 🟡 opponent
23)
24
25// Game holds the full state of one Connect Four match.
26// board is indexed board[row][col]; row 0 is the BOTTOM row.
27type Game struct {
28	ID       int64
29	Red      address // 🔴 creator, moves first
30	Yellow   address // 🟡 opponent
31	Board    [rows][cols]int
32	Turn     int  // whose turn: red or yel
33	Winner   int  // empty until decided; red/yel = winner
34	Draw     bool // true when board full with no winner
35	Finished bool
36}
37
38var (
39	games  = avl.NewTree() // id (zero-padded string) -> *Game
40	nextID int64
41)
42
43func idKey(id int64) string {
44	// zero-pad so avl iteration order matches numeric order for Render lists
45	s := strconv.FormatInt(id, 10)
46	for len(s) < 12 {
47		s = "0" + s
48	}
49	return s
50}
51
52// NewGame creates a match between the caller (🔴) and opponent (🟡).
53// Returns the new game id.
54func NewGame(cur realm, opponent address) int64 {
55	caller := unsafe.PreviousRealm().Address()
56	if opponent == caller {
57		panic("opponent must differ from caller")
58	}
59	if opponent.String() == "" {
60		panic("opponent address is empty")
61	}
62	nextID++
63	g := &Game{
64		ID:     nextID,
65		Red:    caller,
66		Yellow: opponent,
67		Turn:   red,
68	}
69	games.Set(idKey(g.ID), g)
70	chain.Emit("GameCreated",
71		"id", strconv.FormatInt(g.ID, 10),
72		"red", caller.String(),
73		"yellow", opponent.String(),
74	)
75	return g.ID
76}
77
78func getGame(id int64) (*Game, error) {
79	v := games.Get(idKey(id))
80	if v == nil {
81		return nil, errors.New("game not found: " + strconv.FormatInt(id, 10))
82	}
83	return v.(*Game), nil
84}
85
86// Drop places the caller's disc into the given column (0-6).
87// Enforces turn order by caller, rejects full columns, and detects
88// a 4-in-a-row win or a draw.
89func Drop(cur realm, gameID int64, column int) {
90	if column < 0 || column >= cols {
91		panic("column out of range (0-6)")
92	}
93	g, err := getGame(gameID)
94	if err != nil {
95		panic(err.Error())
96	}
97	if g.Finished {
98		panic("game already finished")
99	}
100
101	caller := unsafe.PreviousRealm().Address()
102	var mover int
103	switch caller {
104	case g.Red:
105		mover = red
106	case g.Yellow:
107		mover = yel
108	default:
109		panic("caller is not a player in this game")
110	}
111	if mover != g.Turn {
112		panic("not your turn")
113	}
114
115	// find lowest empty row in the column
116	placed := -1
117	for r := 0; r < rows; r++ {
118		if g.Board[r][column] == empty {
119			g.Board[r][column] = mover
120			placed = r
121			break
122		}
123	}
124	if placed == -1 {
125		panic("column is full")
126	}
127
128	if wins(&g.Board, placed, column, mover) {
129		g.Winner = mover
130		g.Finished = true
131		chain.Emit("GameWon",
132			"id", strconv.FormatInt(g.ID, 10),
133			"winner", caller.String(),
134		)
135	} else if full(&g.Board) {
136		g.Draw = true
137		g.Finished = true
138		chain.Emit("GameDraw", "id", strconv.FormatInt(g.ID, 10))
139	} else {
140		if g.Turn == red {
141			g.Turn = yel
142		} else {
143			g.Turn = red
144		}
145		chain.Emit("DiscDropped",
146			"id", strconv.FormatInt(g.ID, 10),
147			"col", strconv.Itoa(column),
148			"player", caller.String(),
149		)
150	}
151
152	games.Set(idKey(g.ID), g)
153}
154
155func full(b *[rows][cols]int) bool {
156	for c := 0; c < cols; c++ {
157		if b[rows-1][c] == empty {
158			return false
159		}
160	}
161	return true
162}
163
164// wins checks whether the disc just placed at (r,c) for player p completes
165// a run of 4 in any of the four directions.
166func wins(b *[rows][cols]int, r, c, p int) bool {
167	// direction pairs: horizontal, vertical, diag /, diag \
168	dirs := [4][2]int{{0, 1}, {1, 0}, {1, 1}, {1, -1}}
169	for _, d := range dirs {
170		count := 1
171		count += run(b, r, c, d[0], d[1], p)
172		count += run(b, r, c, -d[0], -d[1], p)
173		if count >= 4 {
174			return true
175		}
176	}
177	return false
178}
179
180func run(b *[rows][cols]int, r, c, dr, dc, p int) int {
181	n := 0
182	for i := 1; i < 4; i++ {
183		rr := r + dr*i
184		cc := c + dc*i
185		if rr < 0 || rr >= rows || cc < 0 || cc >= cols {
186			break
187		}
188		if b[rr][cc] != p {
189			break
190		}
191		n++
192	}
193	return n
194}
195
196func glyph(v int) string {
197	switch v {
198	case red:
199		return "🔴"
200	case yel:
201		return "🟡"
202	default:
203		return "·"
204	}
205}
206
207// Render draws the board with 🔴🟡· and shows whose turn / the winner.
208// path "" lists all games; path "<id>" shows a single board.
209func Render(path string) string {
210	path = strings.TrimSpace(strings.Trim(path, "/"))
211	if path == "" {
212		return renderList()
213	}
214	id, err := strconv.ParseInt(path, 10, 64)
215	if err != nil {
216		return "# Connect Four\n\nInvalid game id: `" + path + "`\n"
217	}
218	g, err := getGame(id)
219	if err != nil {
220		return "# Connect Four\n\n" + err.Error() + "\n"
221	}
222	return renderGame(g)
223}
224
225func renderList() string {
226	var sb strings.Builder
227	sb.WriteString("# Connect Four\n\n")
228	sb.WriteString("Two-player Connect Four on a 7×6 board. 🔴 is the game creator, 🟡 the opponent.\n\n")
229	if games.Size() == 0 {
230		sb.WriteString("_No games yet. Call `NewGame(opponent)` to start one._\n")
231		return sb.String()
232	}
233	sb.WriteString("## Games\n\n")
234	sb.WriteString("| ID | 🔴 Red | 🟡 Yellow | Status |\n")
235	sb.WriteString("|----|--------|-----------|--------|\n")
236	games.Iterate("", "", func(_ string, v any) bool {
237		g := v.(*Game)
238		status := ""
239		switch {
240		case g.Winner == red:
241			status = "🔴 won"
242		case g.Winner == yel:
243			status = "🟡 won"
244		case g.Draw:
245			status = "draw"
246		case g.Turn == red:
247			status = "🔴 to move"
248		default:
249			status = "🟡 to move"
250		}
251		sb.WriteString("| [" + strconv.FormatInt(g.ID, 10) + "](/r:" + strconv.FormatInt(g.ID, 10) + ") | " +
252			short(g.Red) + " | " + short(g.Yellow) + " | " + status + " |\n")
253		return false
254	})
255	sb.WriteString("\nOpen a game by its id (e.g. path `1`).\n")
256	return sb.String()
257}
258
259func short(a address) string {
260	s := a.String()
261	if len(s) <= 12 {
262		return "`" + s + "`"
263	}
264	return "`" + s[:8] + "…" + s[len(s)-4:] + "`"
265}
266
267func renderGame(g *Game) string {
268	var sb strings.Builder
269	sb.WriteString("# Connect Four — Game " + strconv.FormatInt(g.ID, 10) + "\n\n")
270	sb.WriteString("- 🔴 Red: `" + g.Red.String() + "`\n")
271	sb.WriteString("- 🟡 Yellow: `" + g.Yellow.String() + "`\n\n")
272
273	// status line
274	switch {
275	case g.Winner == red:
276		sb.WriteString("**🔴 Red wins!**\n\n")
277	case g.Winner == yel:
278		sb.WriteString("**🟡 Yellow wins!**\n\n")
279	case g.Draw:
280		sb.WriteString("**Draw — board full.**\n\n")
281	case g.Turn == red:
282		sb.WriteString("**Turn: 🔴 Red**\n\n")
283	default:
284		sb.WriteString("**Turn: 🟡 Yellow**\n\n")
285	}
286
287	// board top-down (row rows-1 at top, row 0 at bottom)
288	sb.WriteString("```\n")
289	for r := rows - 1; r >= 0; r-- {
290		for c := 0; c < cols; c++ {
291			sb.WriteString(glyph(g.Board[r][c]))
292		}
293		sb.WriteString("\n")
294	}
295	// column indices
296	for c := 0; c < cols; c++ {
297		sb.WriteString(strconv.Itoa(c))
298	}
299	sb.WriteString("\n```\n\n")
300
301	if !g.Finished {
302		sb.WriteString("Drop a disc: `Drop(" + strconv.FormatInt(g.ID, 10) + ", <col 0-6>)`\n")
303	}
304	return sb.String()
305}
306

Raw Package Data

Raw JSON data