Commit 80fb940c authored by refcell.eth's avatar refcell.eth Committed by GitHub

Game state implementation with root node and sequential state (#6161)

construction.
parent 6fbbb7b7
package fault package fault
import (
"errors"
)
var (
// ErrClaimExists is returned when a claim already exists in the game state.
ErrClaimExists = errors.New("claim exists in game state")
// ErrClaimNotFound is returned when a claim does not exist in the game state.
ErrClaimNotFound = errors.New("claim not found in game state")
)
// Game is an interface that represents the state of a dispute game. // Game is an interface that represents the state of a dispute game.
type Game interface { type Game interface {
// Put adds a claim into the game state and returns its parent claim. // Put adds a claim into the game state.
Put(claim Claim) (Claim, error) Put(claim Claim) error
// ClaimPairs returns a list of claim pairs. // ClaimPairs returns a list of claim pairs.
ClaimPairs() []struct { ClaimPairs() []struct {
...@@ -11,3 +23,137 @@ type Game interface { ...@@ -11,3 +23,137 @@ type Game interface {
parent Claim parent Claim
} }
} }
// Node is a node in the game state tree.
type Node struct {
self Claim
children []*Node
}
// gameState is a struct that represents the state of a dispute game.
// The game state implements the [Game] interface.
type gameState struct {
root Node
}
// NewGameState returns a new game state.
// The provided [Claim] is used as the root node.
func NewGameState(root Claim) *gameState {
return &gameState{
root: Node{
self: root,
children: make([]*Node, 0),
},
}
}
// getParent returns the parent of the provided [Claim].
func (g *gameState) getParent(claim Claim) (Claim, error) {
// If the claim is the root node, return an error.
if claim.IsRoot() {
return Claim{}, ErrClaimNotFound
}
// Walk down the tree from the root node to find the parent.
found, err := g.recurseTree(&g.root, claim.Parent)
if err != nil {
return Claim{}, err
}
// Return the parent of the found node.
return found.self, nil
}
// recurseTree recursively walks down the tree from the root node to find the
// node with the provided [Claim].
func (g *gameState) recurseTree(treeNode *Node, claim ClaimData) (*Node, error) {
// Check if the current node is the claim.
if treeNode.self.ClaimData == claim {
return treeNode, nil
}
// Check all children of the current node.
for _, child := range treeNode.children {
// Recurse and drop errors.
n, _ := g.recurseTree(child, claim)
if n != nil {
return n, nil
}
}
// If we reach this point, the claim was not found.
return nil, ErrClaimNotFound
}
// Put adds a claim into the game state.
func (g *gameState) Put(claim Claim) error {
// If the claim is the root node and the node is set, return an error.
if claim.IsRoot() && g.root.self != (Claim{}) {
return ErrClaimExists
}
// Grab the claim's parent.
parent := claim.Parent
// Walk down the tree from the root node to find the parent.
found, err := g.recurseTree(&g.root, parent)
if err != nil {
return err
}
// Check that the node is not already in the tree.
for _, child := range found.children {
if child.self == claim {
return ErrClaimExists
}
}
// Create a new node.
node := Node{
self: claim,
children: make([]*Node, 0),
}
// Add the node to the tree.
found.children = append(found.children, &node)
return nil
}
// recurseTreePairs recursively walks down the tree from the root node
// returning a list of claim and parent pairs.
func (g *gameState) recurseTreePairs(current *Node) []struct {
claim Claim
parent Claim
} {
// Create a list of claim pairs.
pairs := make([]struct {
claim Claim
parent Claim
}, 0)
// Iterate over all children of the current node.
for _, child := range current.children {
// Add the current node to the list of pairs.
pairs = append(pairs, struct {
claim Claim
parent Claim
}{
claim: child.self,
parent: current.self,
})
// Recurse down the tree.
pairs = append(pairs, g.recurseTreePairs(child)...)
}
return pairs
}
// ClaimPairs returns a list of claim pairs.
func (g *gameState) ClaimPairs() []struct {
claim Claim
parent Claim
} {
return g.recurseTreePairs(&g.root)
}
package fault
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func createTestClaims() (Claim, Claim, Claim) {
top := Claim{
ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000768"),
Position: NewPosition(0, 0),
},
Parent: ClaimData{},
}
middle := Claim{
ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000364"),
Position: NewPosition(1, 1),
},
Parent: top.ClaimData,
}
bottom := Claim{
ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000465"),
Position: NewPosition(2, 2),
},
Parent: middle.ClaimData,
}
return top, middle, bottom
}
// TestGame_Put_RootAlreadyExists tests the [Game.Put] method using a [gameState]
// instance errors when the root claim already exists in state.
func TestGame_Put_RootAlreadyExists(t *testing.T) {
// Setup the game state.
top, _, _ := createTestClaims()
g := NewGameState(top)
// Try to put the root claim into the game state again.
err := g.Put(top)
require.ErrorIs(t, err, ErrClaimExists)
}
// TestGame_Put_AlreadyExists tests the [Game.Put] method using a [gameState]
// instance errors when the given claim already exists in state.
func TestGame_Put_AlreadyExists(t *testing.T) {
// Setup the game state.
top, middle, _ := createTestClaims()
g := NewGameState(top)
// Put the next claim into state.
err := g.Put(middle)
require.NoError(t, err)
// Put the claim into the game state again.
err = g.Put(middle)
require.ErrorIs(t, err, ErrClaimExists)
}
// TestGame_Put_ParentsAndChildren tests the [Game.Put] method using a [gameState] instance.
func TestGame_Put_ParentsAndChildren(t *testing.T) {
// Setup the game state.
top, middle, bottom := createTestClaims()
g := NewGameState(top)
// We should not be able to get the parent of the root claim.
parent, err := g.getParent(top)
require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{})
// Put the middle claim into the game state.
// We should expect no parent to exist, yet.
err = g.Put(middle)
require.NoError(t, err)
parent, err = g.getParent(middle)
require.NoError(t, err)
require.Equal(t, parent, top)
// Put the bottom claim into the game state.
// We should expect the parent to be the claim we just added.
err = g.Put(bottom)
require.NoError(t, err)
parent, err = g.getParent(bottom)
require.NoError(t, err)
require.Equal(t, parent, middle)
}
// TestGame_ClaimPairs tests the [Game.ClaimPairs] method using a [gameState] instance.
func TestGame_ClaimPairs(t *testing.T) {
// Setup the game state.
top, middle, bottom := createTestClaims()
g := NewGameState(top)
// Add middle claim to the game state.
err := g.Put(middle)
require.NoError(t, err)
// Add the bottom claim to the game state.
err = g.Put(bottom)
require.NoError(t, err)
// Validate claim pairs.
expected := []struct{ claim, parent Claim }{
{middle, top},
{bottom, middle},
}
pairs := g.ClaimPairs()
require.ElementsMatch(t, expected, pairs)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment