1package connect423import (4 "chain"5 "chain/runtime/unsafe"6 "errors"7 "strconv"8 "strings"910 "gno.land/p/nt/avl/v0"11)1213const (14 cols = 715 rows = 616)1718// cell values19const (20 empty = 021 red = 1 // 🔴 game creator (caller of NewGame)22 yel = 2 // 🟡 opponent23)2425// 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 int6429 Red address // 🔴 creator, moves first30 Yellow address // 🟡 opponent31 Board [rows][cols]int32 Turn int // whose turn: red or yel33 Winner int // empty until decided; red/yel = winner34 Draw bool // true when board full with no winner35 Finished bool36}3738var (39 games = avl.NewTree() // id (zero-padded string) -> *Game40 nextID int6441)4243func idKey(id int64) string {44 // zero-pad so avl iteration order matches numeric order for Render lists45 s := strconv.FormatInt(id, 10)46 for len(s) < 12 {47 s = "0" + s48 }49 return s50}5152// 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.ID76}7778func 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), nil84}8586// Drop places the caller's disc into the given column (0-6).87// Enforces turn order by caller, rejects full columns, and detects88// 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 }100101 caller := unsafe.PreviousRealm().Address()102 var mover int103 switch caller {104 case g.Red:105 mover = red106 case g.Yellow:107 mover = yel108 default:109 panic("caller is not a player in this game")110 }111 if mover != g.Turn {112 panic("not your turn")113 }114115 // find lowest empty row in the column116 placed := -1117 for r := 0; r < rows; r++ {118 if g.Board[r][column] == empty {119 g.Board[r][column] = mover120 placed = r121 break122 }123 }124 if placed == -1 {125 panic("column is full")126 }127128 if wins(&g.Board, placed, column, mover) {129 g.Winner = mover130 g.Finished = true131 chain.Emit("GameWon",132 "id", strconv.FormatInt(g.ID, 10),133 "winner", caller.String(),134 )135 } else if full(&g.Board) {136 g.Draw = true137 g.Finished = true138 chain.Emit("GameDraw", "id", strconv.FormatInt(g.ID, 10))139 } else {140 if g.Turn == red {141 g.Turn = yel142 } else {143 g.Turn = red144 }145 chain.Emit("DiscDropped",146 "id", strconv.FormatInt(g.ID, 10),147 "col", strconv.Itoa(column),148 "player", caller.String(),149 )150 }151152 games.Set(idKey(g.ID), g)153}154155func full(b *[rows][cols]int) bool {156 for c := 0; c < cols; c++ {157 if b[rows-1][c] == empty {158 return false159 }160 }161 return true162}163164// wins checks whether the disc just placed at (r,c) for player p completes165// 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 := 1171 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 true175 }176 }177 return false178}179180func run(b *[rows][cols]int, r, c, dr, dc, p int) int {181 n := 0182 for i := 1; i < 4; i++ {183 rr := r + dr*i184 cc := c + dc*i185 if rr < 0 || rr >= rows || cc < 0 || cc >= cols {186 break187 }188 if b[rr][cc] != p {189 break190 }191 n++192 }193 return n194}195196func glyph(v int) string {197 switch v {198 case red:199 return "🔴"200 case yel:201 return "🟡"202 default:203 return "·"204 }205}206207// 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}224225func renderList() string {226 var sb strings.Builder227 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 false254 })255 sb.WriteString("\nOpen a game by its id (e.g. path `1`).\n")256 return sb.String()257}258259func 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}266267func renderGame(g *Game) string {268 var sb strings.Builder269 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")272273 // status line274 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 }286287 // 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 indices296 for c := 0; c < cols; c++ {297 sb.WriteString(strconv.Itoa(c))298 }299 sb.WriteString("\n```\n\n")300301 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}306Drop(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, gameID int64, column int)
NewGame(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, opponent string) int64
Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.