Skip to content

Commit

Permalink
import all 4 files by moul from gnolang#613
Browse files Browse the repository at this point in the history
  • Loading branch information
grepsuzette committed Jun 14, 2024
1 parent a1ab6a1 commit 62d30f0
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 0 deletions.
163 changes: 163 additions & 0 deletions examples/gno.land/r/demo/games/tictactoe/game.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package tictactoe

import (
"errors"
"std"
)

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
}

// 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 errors.New("slot already used")
}

// 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) checkLastMoveWon(posX, posY int) bool {
// assumes the game wasn't won yet, and that's 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 tl == c && c == br {
return true
}
if 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) IsDraw() bool { return g.turnCtr >= 8 && g.winnerIdx == 0 }
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 := 0; y < 3; 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 x*3 + y }
85 changes: 85 additions & 0 deletions examples/gno.land/r/demo/games/tictactoe/game_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package tictactoe

import (
"strings"
"testing"

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

func TestGame(t *testing.T) {
var (
addr1 = testutils.TestAddress("addr1")
addr2 = testutils.TestAddress("addr2")
addr3 = testutils.TestAddress("addr3")
)

game, err := NewGame(addr1, addr1)
assertErr(t, err)

game, err = NewGame(addr2, addr3)
assertNoErr(t, err)

assertFalse(t, game.IsOver())
assertFalse(t, game.IsDraw())
assertErr(t, game.Play(addr3, 0, 0)) // addr2's turn
assertErr(t, game.Play(addr2, -1, 0)) // invalid location
assertErr(t, game.Play(addr2, 3, 0)) // invalid location
assertErr(t, game.Play(addr2, 0, -1)) // invalid location
assertErr(t, game.Play(addr2, 0, 3)) // invalid location
assertNoErr(t, game.Play(addr2, 1, 1)) // first move
assertErr(t, game.Play(addr2, 2, 2)) // addr3's turn
assertErr(t, game.Play(addr3, 1, 1)) // slot already used
assertNoErr(t, game.Play(addr3, 0, 0)) // second move
assertNoErr(t, game.Play(addr2, 1, 2)) // third move
assertNoErr(t, game.Play(addr3, 0, 1)) // fourth move
assertFalse(t, game.IsOver())
assertNoErr(t, game.Play(addr2, 1, 0)) // fifth move (win)
assertTrue(t, game.IsOver())
assertFalse(t, game.IsDraw())

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

func assertEqualString(t *testing.T, expected, got string) {
t.Helper()
expected = strings.TrimSpace(expected)
got = strings.TrimSpace(got)
if expected != got {
t.Errorf("expected %q, got %q", expected, got)
}
}

func assertNoErr(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Errorf("got err: %v", err)
}
}

func assertErr(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Errorf("expected an error, got nil")
}
}

func assertTrue(t *testing.T, val bool) {
t.Helper()
if !val {
t.Errorf("expected true, got false")
}
}

func assertFalse(t *testing.T, val bool) {
t.Helper()
if val {
t.Errorf("expected false, got true")
}
}
85 changes: 85 additions & 0 deletions examples/gno.land/r/demo/games/tictactoe/tictactoe.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package tictactoe

import (
"errors"
"std"
"strconv"
"strings"

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

var (
gameCtr int // game counter
games avl.Tree // int(gameID) -> Game
)

// Challenge creates a new game, and return a gameID.
//
// If ugnot are sent, they will be stored for bet and require the opponent to send the same amount.
func Challenge(opponent std.Address) string {
caller := std.GetOrigCaller()
g, err := NewGame(caller, opponent)
if err != nil {
panic(err)
}
// TODO: handle "sent gnots" for bets.

gameCtr++
gameID := strconv.Itoa(gameCtr)

games.Set(gameID, g)
return gameID
}

// Join joins a previously created game.
//
// Caller should have be the opponent of gameID.
func Join(gameID string) {
caller := std.GetOrigCaller()

g, err := getGameByID(gameID)
if err != nil {
panic("no such game: " + err.Error())
}
// TODO: handle "sent".
// TODO: determines starting player randomly.
// TODO: check for already accepted.

if g.player2 != caller {
panic("only invited opponent can join the game.")
}

g.start()
games.Set(gameID, g)
}

func getGameByID(id string) (*Game, error) {
obj, found := games.Get(id)
if !found {
return nil, errors.New("game not found.")
}
return obj.(*Game), nil
}

func Render(path string) string {
path = strings.TrimSpace(path)
parts := strings.Split(path, "/")
partN := len(parts)

switch {
case partN == 0:
// TODO: leaderboard
// TODO: "new game" link
// TODO: "join challenge" link
// TODO: vanity metrics
// TODO: last N active games
return "home"
case partN == 2 && parts[0] == "game":
gameID := parts[1]
_ = gameID
// FIXME: continue implementation
// games.Get()
}
return "404"
}
7 changes: 7 additions & 0 deletions examples/gno.land/r/demo/games/tictactoe/tictactoe_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tictactoe

import "testing"

func TestRender(t *testing.T) {

}

0 comments on commit 62d30f0

Please sign in to comment.