Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

connect4.gno

6.77 Kb Β· 305 lines
  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}