← Back to Realms

Realm

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

Overview

Realm Path
gno.land/r/moul/x/daily/connect4/v1
Exported Functions
3
State Entries
7
Source Files
3
Total Package Entries
17

Exported Functions

3 exported functions

State

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

Raw Package Data

Raw JSON data