Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d28253d
add p/demo/ternary
grepsuzette Jul 9, 2024
6b42940
feat(examples): add stateless r/demo/games/tictactoe 1P-VS-CPU
grepsuzette Jul 9, 2024
0dca9cb
typo
grepsuzette Jul 9, 2024
35dc9f8
minor
grepsuzette Jul 10, 2024
fc26365
more responsive (use css flex)
grepsuzette Jul 12, 2024
606af47
last minute addendum
grepsuzette Jul 12, 2024
68e8dde
Merge branch 'master' into tictactoe
grepsuzette Aug 23, 2024
ee549ed
remove submodule gfx
grepsuzette Aug 23, 2024
83333f6
lint
grepsuzette Aug 23, 2024
450037a
Merge remote-tracking branch 'refs/remotes/grepsuzette/tictactoe' int…
grepsuzette Aug 23, 2024
49e76b5
delete useless
grepsuzette Aug 23, 2024
5b12a63
run make fmt
grepsuzette Aug 23, 2024
f5735c9
gno mod tidy
grepsuzette Aug 23, 2024
8041c7c
tidy
grepsuzette Aug 23, 2024
748b84f
tidy
grepsuzette Aug 23, 2024
b193c4e
itidy
grepsuzette Aug 23, 2024
9b47e88
Merge branch 'master' into tictactoe
grepsuzette Aug 26, 2024
8f7fbd9
add r/games/shifumi to the list of games
grepsuzette Sep 1, 2024
412616e
Merge remote-tracking branch 'refs/remotes/grepsuzette/tictactoe' int…
grepsuzette Sep 1, 2024
21c00cf
type
grepsuzette Sep 5, 2024
a162ef2
Merge branch 'master' into tictactoe
thehowl Jan 16, 2025
2ec9d50
fix gno.mod for tests to pass
grepsuzette Jan 19, 2025
f195beb
fix lint problem
grepsuzette Jan 19, 2025
455bc22
mod
grepsuzette Jan 19, 2025
11f1a95
move files from /[rp]/demo to /[rp]/grepsuzette
grepsuzette Jan 21, 2025
5643b7c
more
grepsuzette Jan 21, 2025
2dba381
more
grepsuzette Jan 21, 2025
2f9e850
index
grepsuzette Jan 21, 2025
f1d211b
r/grepsuzette/querystring
grepsuzette Jan 21, 2025
071ab29
ok, tested with `gnodev -web-html`
grepsuzette Jan 22, 2025
b22c80d
more
grepsuzette Jan 22, 2025
1e1f9ad
Merge branch 'master' into tictactoe
grepsuzette Jan 23, 2025
ebae8fa
Merge branch 'master' into tictactoe
thehowl Sep 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions examples/gno.land/p/grepsuzette/games/tictactoe/game.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package tictactoe

import (
"errors"
"std"

"gno.land/p/demo/ufmt"
)

// this file is @moul's work in #613
// a few changes and bugfixes have been made

type Game struct {
player1, player2 std.Address
board [9]rune // 0=empty, 1=player1, 2=player2
turnCtr int
winnerIdx int
}

func NewGame(player1, player2 std.Address) (*Game, error) {
if player1 == player2 {
return nil, errors.New("cannot fight against self")
}

g := Game{
player1: player1,
player2: player2,
winnerIdx: -1,
turnCtr: -1,
}
return &g, nil
}

// Partially recover a game
// The game is guaranteed to be legit in terms of number of tiles 1 and 2
// No winning detection is implemented here however
func RecoverGame(player1, player2 std.Address, board string) (*Game, error) {
g, e := NewGame(player1, player2)
if e != nil {
return nil, e
}
if len(board) != 9 {
return nil, ufmt.Errorf("invalid board length: %d", len(board))
}
num1, num2 := 0, 0
runes := [9]rune{}
for i, c := range board {
switch c {
case rune(0), '_', '-':
runes[i] = rune(0)
case rune(1), 'O', 'o':
num1 += 1
runes[i] = rune(1)
case rune(2), 'X', 'x':
num2 += 1
runes[i] = rune(2)
default:
return nil, errors.New("invalid rune")
}
}
if num1 != num2 && num1 != num2+1 {
return nil, errors.New("invalid number of x and o")
}
g.board = runes
g.turnCtr = num1 + num2
g.winnerIdx = -1
return g, nil
}

// start sets turnCtr to 0.
func (g *Game) Start() {
if g.turnCtr != -1 {
panic("game already started")
}
g.turnCtr = 0
}

func (g *Game) Play(player std.Address, posX, posY int) error {
if !g.Started() {
return errors.New("game not started")
}

if g.Turn() != player {
return errors.New("invalid turn")
}

if g.IsOver() {
return errors.New("game over")
}

// are posX and posY valid
if posX < 0 || posY < 0 || posX > 2 || posY > 2 {
return errors.New("posX and posY should be 0, 1 or 2")
}

// is slot already used?
idx := xyToIdx(posX, posY)
if g.board[idx] != 0 {
return ufmt.Errorf("slot already used (%d, %d)", posX, posY)
}

// play
playerVal := rune(g.turnCtr%2) + 1 // player1=1, player2=2
g.board[idx] = playerVal

// check if win
if g.checkLastMoveWon(posX, posY) {
g.winnerIdx = g.turnCtr
}

// change turn
g.turnCtr++
return nil
}

func (g Game) WouldWin(side rune, x, y int) bool {
idx := xyToIdx(x, y)
if g.board[idx] != rune(0) {
panic("tile should be empty")
}
// place rune temporarily
g.board[idx] = side
b := g.checkLastMoveWon(x, y)
g.board[idx] = rune(0)
return b
}

func (g Game) checkLastMoveWon(posX, posY int) bool {
// assumes the game wasn't won yet, and that the move was already applied.

// check vertical line
{
a := g.At(posX, 0)
b := g.At(posX, 1)
c := g.At(posX, 2)
if a == b && b == c {
return true
}
}

// check horizontal line
{
a := g.At(0, posY)
b := g.At(1, posY)
c := g.At(2, posY)
if a == b && b == c {
return true
}
}

// diagonals
{
tl := g.At(0, 0)
tr := g.At(0, 2)
bl := g.At(2, 0)
br := g.At(2, 2)
c := g.At(1, 1)
if posX == posY && tl == c && c == br {
return true
}
if posX+posY == 2 && tr == c && c == bl {
return true
}
}
return false
}

func (g Game) At(posX, posY int) rune { return g.board[xyToIdx(posX, posY)] }
func (g Game) Winner() std.Address { return g.PlayerByIndex(g.winnerIdx) }
func (g Game) Turn() std.Address { return g.PlayerByIndex(g.turnCtr) }
func (g Game) TurnNumber() int { return g.turnCtr }
func (g Game) IsDraw() bool { return g.turnCtr > 8 && g.winnerIdx == -1 }
func (g Game) Started() bool { return g.turnCtr >= 0 }

func (g Game) IsOver() bool {
// draw
if g.turnCtr > 8 {
return true
}

// winner
return g.Winner() != std.Address("")
}

func (g Game) Output() string {
output := ""

for y := 2; y >= 0; y-- {
for x := 0; x < 3; x++ {
val := g.At(x, y)
switch val {
case 0:
output += "-"
case 1:
output += "O"
case 2:
output += "X"
}
}
output += "\n"
}

return output
}

func (g Game) PlayerByIndex(idx int) std.Address {
switch idx % 2 {
case 0:
return g.player1
case 1:
return g.player2
default:
return std.Address("")
}
}

func xyToIdx(x, y int) int { return y*3 + x }
80 changes: 80 additions & 0 deletions examples/gno.land/p/grepsuzette/games/tictactoe/game_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package tictactoe

import (
"testing"

"gno.land/p/demo/testutils"
"gno.land/p/demo/uassert"
)

var (
addr1 = testutils.TestAddress("addr1")
addr2 = testutils.TestAddress("addr2")
addr3 = testutils.TestAddress("addr3")
)

func TestGame(t *testing.T) {
game, err := NewGame(addr1, addr1)
uassert.Error(t, err)

game, err = NewGame(addr2, addr3)
uassert.NoError(t, err)

uassert.False(t, game.IsOver())
uassert.False(t, game.IsDraw())
game.Start()
uassert.Error(t, game.Play(addr3, 0, 0)) // addr2's turn
uassert.Error(t, game.Play(addr2, -1, 0)) // invalid location
uassert.Error(t, game.Play(addr2, 3, 0)) // invalid location
uassert.Error(t, game.Play(addr2, 0, -1)) // invalid location
uassert.Error(t, game.Play(addr2, 0, 3)) // invalid location
uassert.NoError(t, game.Play(addr2, 1, 1)) // first move
uassert.Error(t, game.Play(addr2, 2, 2)) // addr3's turn
uassert.Error(t, game.Play(addr3, 1, 1)) // slot already used
uassert.NoError(t, game.Play(addr3, 0, 0)) // second move
uassert.NoError(t, game.Play(addr2, 1, 2)) // third move
uassert.NoError(t, game.Play(addr3, 0, 1)) // fourth move
uassert.False(t, game.IsOver())
uassert.NoError(t, game.Play(addr2, 1, 0)) // fifth move (win)
uassert.True(t, game.IsOver())
uassert.False(t, game.IsDraw())

expected := `-O-
XO-
XO-
`
got := game.Output()
uassert.Equal(t, expected, got)
}

func TestRecoverGame(t *testing.T) {
for _, o := range []struct {
repr, err string
}{
{"", "error"},
{"--", "error"},
{"---", "error"},
{"-----", "error"},
{"--------", "error"},
{"---------", ""},
{"XX-------", "error"},
{"OO-------", "error"},
{"XO-X-----", "error"}, // O plays first
{"XO-O-----", ""}, // valid from there on
{"XOXO-----", ""},
{"XOXOO----", ""},
{"XOXOO-X--", ""},
{"XOXOOOX--", ""}, // circles won but the function doesn't care
{"XOXOOOX-X", ""},
{"XOXOOOXOX", ""}, // circles won a second time
{"XOXOOOXOXX", "error"}, // too long (10 squares)
} {
g, e := RecoverGame(addr1, addr2, o.repr)
if o.err == "error" {
uassert.Error(t, e, "repr=", o.repr)
} else {
uassert.NoError(t, e, "repr=", o.repr)
uassert.True(t, g != nil, "repr=", o.repr)
}
}
}
7 changes: 7 additions & 0 deletions examples/gno.land/p/grepsuzette/games/tictactoe/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module gno.land/p/grepsuzette/games/tictactoe

// require (
// gno.land/p/demo/testutils v0.0.0-latest
// gno.land/p/demo/uassert v0.0.0-latest
// gno.land/p/demo/ufmt v0.0.0-latest
// )
Loading
Loading