forked from gnolang/gno
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
import all 4 files by moul from gnolang#613
- Loading branch information
1 parent
a1ab6a1
commit 62d30f0
Showing
4 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package tictactoe | ||
|
||
import "testing" | ||
|
||
func TestRender(t *testing.T) { | ||
|
||
} |