package connect4 import ( "chain" "chain/runtime/unsafe" "errors" "strconv" "strings" "gno.land/p/moul/kit/ui/v0" "gno.land/p/moul/kit/store/v0" ) const ( cols = 7 rows = 6 ) // cell values const ( empty = 0 red = 1 // ๐Ÿ”ด game creator (caller of NewGame) yel = 2 // ๐ŸŸก opponent ) // Game holds the full state of one Connect Four match. // board is indexed board[row][col]; row 0 is the BOTTOM row. // It carries no ID field: the id belongs to the store, which hands it back on // lookup and iteration. type Game struct { Red address // ๐Ÿ”ด creator, moves first Yellow address // ๐ŸŸก opponent Board [rows][cols]int Turn int // whose turn: red or yel Winner int // empty until decided; red/yel = winner Draw bool // true when board full with no winner Finished bool } // games assigns the game ids. v1 kept its own nextID plus an idKey() that // zero-padded to width 12, which stopped ordering the Render list past 10^12. var games = store.Named("game") // NewGame creates a match between the caller (๐Ÿ”ด) and opponent (๐ŸŸก). // Returns the new game id. func NewGame(cur realm, opponent address) int64 { caller := unsafe.PreviousRealm().Address() if opponent == caller { panic("opponent must differ from caller") } if opponent.String() == "" { panic("opponent address is empty") } id := games.Add(&Game{ Red: caller, Yellow: opponent, Turn: red, }) chain.Emit("GameCreated", "id", id.String(), "red", caller.String(), "yellow", opponent.String(), ) return int64(id) } func getGame(id int64) (*Game, error) { v, ok := games.Get(store.ID(id)) if !ok { return nil, errors.New("game not found: " + strconv.FormatInt(id, 10)) } return v.(*Game), nil } // Drop places the caller's disc into the given column (0-6). // Enforces turn order by caller, rejects full columns, and detects // a 4-in-a-row win or a draw. func Drop(cur realm, gameID int64, column int) { if column < 0 || column >= cols { panic("column out of range (0-6)") } g, err := getGame(gameID) if err != nil { panic(err.Error()) } if g.Finished { panic("game already finished") } caller := unsafe.PreviousRealm().Address() var mover int switch caller { case g.Red: mover = red case g.Yellow: mover = yel default: panic("caller is not a player in this game") } if mover != g.Turn { panic("not your turn") } // find lowest empty row in the column placed := -1 for r := 0; r < rows; r++ { if g.Board[r][column] == empty { g.Board[r][column] = mover placed = r break } } if placed == -1 { panic("column is full") } if wins(&g.Board, placed, column, mover) { g.Winner = mover g.Finished = true chain.Emit("GameWon", "id", strconv.FormatInt(gameID, 10), "winner", caller.String(), ) } else if full(&g.Board) { g.Draw = true g.Finished = true chain.Emit("GameDraw", "id", strconv.FormatInt(gameID, 10)) } else { if g.Turn == red { g.Turn = yel } else { g.Turn = red } chain.Emit("DiscDropped", "id", strconv.FormatInt(gameID, 10), "col", strconv.Itoa(column), "player", caller.String(), ) } } func full(b *[rows][cols]int) bool { for c := 0; c < cols; c++ { if b[rows-1][c] == empty { return false } } return true } // wins checks whether the disc just placed at (r,c) for player p completes // a run of 4 in any of the four directions. func wins(b *[rows][cols]int, r, c, p int) bool { // direction pairs: horizontal, vertical, diag /, diag \ dirs := [4][2]int{{0, 1}, {1, 0}, {1, 1}, {1, -1}} for _, d := range dirs { count := 1 count += run(b, r, c, d[0], d[1], p) count += run(b, r, c, -d[0], -d[1], p) if count >= 4 { return true } } return false } func run(b *[rows][cols]int, r, c, dr, dc, p int) int { n := 0 for i := 1; i < 4; i++ { rr := r + dr*i cc := c + dc*i if rr < 0 || rr >= rows || cc < 0 || cc >= cols { break } if b[rr][cc] != p { break } n++ } return n } func glyph(v int) string { switch v { case red: return "๐Ÿ”ด" case yel: return "๐ŸŸก" default: return "ยท" } } // Render draws the board with ๐Ÿ”ด๐ŸŸกยท and shows whose turn / the winner. // path "" lists all games; path "" shows a single board. func Render(path string) string { path = strings.TrimSpace(strings.Trim(path, "/")) if path == "" { return renderList() } id, ok := store.ParseID(path) if !ok { return "# Connect Four\n\nInvalid game id: `" + path + "`\n" } g, err := getGame(int64(id)) if err != nil { return "# Connect Four\n\n" + err.Error() + "\n" } return renderGame(id, g) } func renderList() string { var sb strings.Builder sb.WriteString("# Connect Four\n\n") sb.WriteString("Two-player Connect Four on a 7ร—6 board. ๐Ÿ”ด is the game creator, ๐ŸŸก the opponent.\n\n") if games.Len() == 0 { sb.WriteString("_No games yet. Call `NewGame(opponent)` to start one._\n") return sb.String() } sb.WriteString("## Games\n\n") sb.WriteString("| ID | ๐Ÿ”ด Red | ๐ŸŸก Yellow | Status |\n") sb.WriteString("|----|--------|-----------|--------|\n") games.Each(func(id store.ID, v any) { g := v.(*Game) status := "" switch { case g.Winner == red: status = "๐Ÿ”ด won" case g.Winner == yel: status = "๐ŸŸก won" case g.Draw: status = "draw" case g.Turn == red: status = "๐Ÿ”ด to move" default: status = "๐ŸŸก to move" } sb.WriteString("| [" + id.String() + "](/r:" + id.String() + ") | " + ui.Addr(g.Red) + " | " + ui.Addr(g.Yellow) + " | " + status + " |\n") }) sb.WriteString("\nOpen a game by its id (e.g. path `1`).\n") return sb.String() } func renderGame(id store.ID, g *Game) string { var sb strings.Builder sb.WriteString("# Connect Four โ€” Game " + id.String() + "\n\n") sb.WriteString("- ๐Ÿ”ด Red: `" + g.Red.String() + "`\n") sb.WriteString("- ๐ŸŸก Yellow: `" + g.Yellow.String() + "`\n\n") // status line switch { case g.Winner == red: sb.WriteString("**๐Ÿ”ด Red wins!**\n\n") case g.Winner == yel: sb.WriteString("**๐ŸŸก Yellow wins!**\n\n") case g.Draw: sb.WriteString("**Draw โ€” board full.**\n\n") case g.Turn == red: sb.WriteString("**Turn: ๐Ÿ”ด Red**\n\n") default: sb.WriteString("**Turn: ๐ŸŸก Yellow**\n\n") } // board top-down (row rows-1 at top, row 0 at bottom) sb.WriteString("```\n") for r := rows - 1; r >= 0; r-- { for c := 0; c < cols; c++ { sb.WriteString(glyph(g.Board[r][c])) } sb.WriteString("\n") } // column indices for c := 0; c < cols; c++ { sb.WriteString(strconv.Itoa(c)) } sb.WriteString("\n```\n\n") if !g.Finished { sb.WriteString("Drop a disc: `Drop(" + id.String() + ", )`\n") } return sb.String() }