Search Apps Documentation Source Content File Folder Download Copy Actions Download

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, ok := games.Get(idKey(id))
 80	if !ok {
 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}