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

v0 source pure

Package gno4 is a Connect Four engine: the rules, and nothing else.

Readme View source

gno.land/p/moul/gno4/v0

Connect Four, as rules and nothing else: New, Drop, Winner, Encode, Decode, SVG.

1import "gno.land/p/moul/gno4/v0"
2
3b, _ := gno4.Decode("1122334") // replay a whole game from 7 bytes
4b.Winner()                     // -> gno4.Red
5b.Line()                       // -> the four cells that won
6b.Image("board")               // -> ![board](data:image/svg+xml;base64,...)

A realm that wants a game needs three different things: the rules, the authority to decide who may move, and a way to show the result. Only the first is reusable, and only the first can be tested without a chain. This package is the first, alone: it imports no chain package, takes no address, reads no height, and has no idea a block exists.

A game is its move list, deliberately. Encode returns one digit per move and nothing else; Decode replays them. The position, the winner and the winning line are all derived, never stored, so there is exactly one representation and no second encoder that can drift from it. A whole game is at most 42 bytes, which is small enough that a realm stores the string and replays it rather than persisting a board.

Four behaviours worth knowing before you use it:

  • Drop is the only mutator. Every illegal position this package could reach is refused in one place, which is why Decode can be trusted: it replays through Drop, so an encoding describing an impossible game (a column overflowing, a move after the win) is rejected rather than becoming a board no rule allows.
  • At returns Empty out of range instead of panicking. Render loops walk past the edge on purpose, and a Render that panics is a realm page that is unreadable forever.
  • The winner is computed once, by the Drop that creates it. A finished game is rendered far more often than it is played, and rescanning 69 lines per read is gas spent to learn something the board already knew.
  • WinningMove is one ply deep, on purpose. It answers "can I win now" and "must I block now" for seven scans and no allocation. Real search belongs in the front-end, where nobody pays gas to think.

SVG draws the board with p/nt/svg and Image wraps it as a data:image/svg+xml;base64 markdown image, which is the one inline data URI gnoweb allows. It is generated at render time, so it costs no storage and there is no asset to go missing later.

Live demo: r/moul/gno4.

Overview

Package gno4 is a Connect Four engine: the rules, and nothing else.

It imports no chain package, takes no address and reads no height, so it is unit-testable with no node and a realm that uses it stays thin. Everything a caller needs to know about a position is derivable from a Board, and a whole game round-trips through a string of at most 42 digits (Board.Encode).

Live demo: r/moul/gno4(/r/moul/gno4).

Constants 2

const Cols, Rows

1const (
2	Cols = 7
3	Rows = 6
4)
source

Cols and Rows are the standard board. They are constants rather than fields because every rule below, the win scan in particular, is written against them; a variable board is a different package, not a flag on this one.

Variables 1

Functions 2

func Decode

1func Decode(s string) (*Board, error)
source

Decode replays an encoding produced by Board.Encode.

It replays through Board.Drop rather than writing cells directly, so an encoding that describes an impossible game (a column overflowing, a move after the win) is rejected here instead of becoming a board no rule allows. Nothing else in this package can construct an illegal position.

func New

1func New() *Board
source

New returns an empty board. Red moves first.

Types 2

type Board

struct
1type Board struct {
2	cells  [Cols * Rows]Disc
3	moves  []int
4	winner Disc
5	line   []int
6}
source

Board is a position plus the moves that produced it.

The winner and the winning line are computed once, by the Drop that creates them, and stored. A realm renders a finished game far more often than it plays a move, and rescanning 69 lines on every Render is gas spent to learn something the board already knew.

Methods on Board

func At

method on Board
1func (b *Board) At(col, row int) Disc
source

At returns the disc at (col, row), counting rows from the bottom. Out of range is Empty rather than a panic: At is called from render loops that walk past the edge on purpose, and a Render that panics is a permanently unreadable page.

func CanDrop

method on Board
1func (b *Board) CanDrop(col int) bool
source

CanDrop reports whether the current player may play col right now.

func Clone

method on Board
1func (b *Board) Clone() *Board
source

Clone returns an independent copy. Callers that search (a hint, a bot) mutate a clone; a Board handed out by a realm is never a scratch pad.

func Draw

method on Board
1func (b *Board) Draw() bool
source

Draw reports whether the board filled with no winner.

func Drop

method on Board
1func (b *Board) Drop(col int) (int, error)
source

Drop plays the current player's disc into col and returns the row it landed on. It is the only method that mutates a Board, so every illegal state this package could reach is refused in one place.

func Emoji

method on Board
1func (b *Board) Emoji() string
source

Emoji returns the board as seven columns of coloured squares, top row first.

The text fallback, for the places an image cannot go: a chain event, a terminal, a commit message, a diff. It is also what makes the whole package legible in a test failure.

func Encode

method on Board
1func (b *Board) Encode() string
source

Encode returns the whole game as one string: the 1-based column of each move, in order. An empty game is the empty string, a full game is 42 bytes.

The position is not stored, because it is not information: replaying the moves reproduces it exactly, including the winner and the winning line. That is what lets a match be a URL, a chain event and a test fixture without three encoders that can disagree.

func Full

method on Board
1func (b *Board) Full() bool
source

Full reports whether all 42 cells are taken.

func Height

method on Board
1func (b *Board) Height(col int) int
source

Height returns how many discs are stacked in col.

func Image

method on Board
1func (b *Board) Image(alt string) string
source

Image returns the board as a markdown image, ready to drop into a realm's Render output. The SVG travels inline as a data URI, which gnoweb allows for image/svg+xml specifically, so the board needs no hosting and no asset pipeline: there is nothing to go missing later, and no storage is spent on it because it is drawn at render time rather than stored.

func Line

method on Board
1func (b *Board) Line() []int
source

Line returns the four cell indexes that won, or nil. An index is row*Cols+col, the same order Board.At uses.

func MoveCount

method on Board
1func (b *Board) MoveCount() int
source

MoveCount returns how many discs are on the board.

func Moves

method on Board
1func (b *Board) Moves() []int
source

Moves returns a copy of the move list, as zero-based column indexes.

func Over

method on Board
1func (b *Board) Over() bool
source

Over reports whether the game is decided: someone won, or the board filled.

func SVG

method on Board
1func (b *Board) SVG() string
source

SVG returns the board as standalone <svg> markup.

It is a drawing of a position, not a control: there is no script, no link and no interactivity, because gnoweb serves this through a data URI and would not run any of it. The clickable column menu is the realm's job, as ordinary markdown links next to the image.

func String

method on Board
1func (b *Board) String() string
source

String renders the board as seven columns of ASCII, top row first, the way a player looks at it. Empty is '.', Red is 'R', Yellow is 'Y'.

This is the debugging view and the one tests assert against. What gnoweb shows is Board.Image; what a terminal or a diff shows is this.

func Turn

method on Board
1func (b *Board) Turn() Disc
source

Turn returns whose move it is. Red on even move counts, Yellow on odd.

func Winner

method on Board
1func (b *Board) Winner() Disc
source

Winner returns the disc that made four in a row, or Empty.

func WinningMove

method on Board
1func (b *Board) WinningMove(d Disc) int
source

WinningMove returns a column where d wins on the spot, or -1.

It is deliberately one ply deep. That covers the two things a player wants from a board they are looking at ("can I win now", "can they win now, so I must block") and costs seven clones; a real search belongs off-chain, in the front-end, where nobody pays gas to think.

type Disc

ident
1type Disc byte
source

Disc identifies whose disc occupies a cell. Empty is the zero value, so a freshly allocated Board is a legal empty board with no constructor call.

Methods on Disc

func Other

method on Disc
1func (d Disc) Other() Disc
source

Other returns the opponent of d, and Empty for Empty.

func String

method on Disc
1func (d Disc) String() string
source

Imports 4

Source Files 5