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.52 Kb Β· 284 lines
  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}