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.

connect4.gno

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