1
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from tugo.encoders.base import *
|
||||
#from tugo.encoders.alphago import *
|
||||
#from tugo.encoders.betago import *
|
||||
from tugo.encoders.oneplane import *
|
||||
from tugo.encoders.sevenplane import *
|
||||
from tugo.encoders.simple import *
|
||||
@@ -0,0 +1,141 @@
|
||||
from tugo.encoders.base import Encoder
|
||||
from tugo.encoders.utils import is_ladder_escape, is_ladder_capture
|
||||
from tugo.gotypes import Point, Player
|
||||
from tugo.goboard_fast import Move
|
||||
from tugo.agent.helpers_fast import is_point_an_eye
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
"""
|
||||
Feature name num of planes Description
|
||||
Stone colour 3 Player stone / opponent stone / empty
|
||||
Ones 1 A constant plane filled with 1
|
||||
Zeros 1 A constant plane filled with 0
|
||||
Sensibleness 1 Whether a move is legal and does not fill its own eyes
|
||||
Turns since 8 How many turns since a move was played
|
||||
Liberties 8 Number of liberties (empty adjacent points)
|
||||
Liberties after move 8 Number of liberties after this move is played
|
||||
Capture size 8 How many opponent stones would be captured
|
||||
Self-atari size 8 How many of own stones would be captured
|
||||
Ladder capture 1 Whether a move at this point is a successful ladder capture
|
||||
Ladder escape 1 Whether a move at this point is a successful ladder escape
|
||||
"""
|
||||
|
||||
FEATURE_OFFSETS = {
|
||||
"stone_color": 0,
|
||||
"ones": 3,
|
||||
"zeros": 4,
|
||||
"sensibleness": 5,
|
||||
"turns_since": 6,
|
||||
"liberties": 14,
|
||||
"liberties_after": 22,
|
||||
"capture_size": 30,
|
||||
"self_atari_size": 38,
|
||||
"ladder_capture": 46,
|
||||
"ladder_escape": 47,
|
||||
"current_player_color": 48,
|
||||
}
|
||||
|
||||
|
||||
def offset(feature):
|
||||
return FEATURE_OFFSETS[feature]
|
||||
|
||||
|
||||
class AlphaGoEncoder(Encoder):
|
||||
def __init__(self, board_size=(19, 19), use_player_plane=True):
|
||||
self.board_width, self.board_height = board_size
|
||||
self.use_player_plane = use_player_plane
|
||||
self.num_planes = 48 + use_player_plane
|
||||
|
||||
def name(self):
|
||||
return "alphago"
|
||||
|
||||
def encode(self, game_state):
|
||||
board_tensor = torch.zeros((self.num_planes, self.board_height, self.board_width))
|
||||
for r in range(self.board_height):
|
||||
for c in range(self.board_width):
|
||||
point = Point(row=r + 1, col=c + 1)
|
||||
|
||||
go_string = game_state.board.get_go_string(point)
|
||||
if go_string and go_string.color == game_state.next_player:
|
||||
board_tensor[offset("stone_color"), r, c] = 1
|
||||
elif go_string and go_string.color == game_state.next_player.other:
|
||||
board_tensor[offset("stone_color") + 1, r, c] = 1
|
||||
else:
|
||||
board_tensor[offset("stone_color") + 2, r, c] = 1
|
||||
|
||||
board_tensor[offset("ones")] = self.ones()
|
||||
board_tensor[offset("zeros")] = self.zeros()
|
||||
|
||||
if not is_point_an_eye(game_state.board, point, game_state.next_player):
|
||||
board_tensor[offset("sensibleness"), r, c] = 1
|
||||
|
||||
ages = min(game_state.board.move_ages.get(r, c), 8)
|
||||
if ages > 0:
|
||||
board_tensor[offset("turns_since") + int(ages), r, c] = 1
|
||||
|
||||
if game_state.board.get_go_string(point):
|
||||
liberties = min(game_state.board.get_go_string(point).num_liberties, 8)
|
||||
board_tensor[offset("liberties") + liberties, r, c] = 1
|
||||
|
||||
move = Move(point)
|
||||
if game_state.is_valid_move(move):
|
||||
new_state = game_state.apply_move(move)
|
||||
liberties = min(new_state.board.get_go_string(point).num_liberties, 8)
|
||||
board_tensor[offset("liberties_after") + liberties, r, c] = 1
|
||||
|
||||
adjacent_strings = [game_state.board.get_go_string(nb) for nb in point.neighbors()]
|
||||
capture_count = 0
|
||||
for go_string in adjacent_strings:
|
||||
other_player = game_state.next_player.other
|
||||
if go_string and go_string.num_liberties == 1 and go_string.color == other_player:
|
||||
capture_count += len(go_string.stones)
|
||||
capture_count = min(capture_count, 8)
|
||||
board_tensor[offset("capture_size") + capture_count, r, c] = 1
|
||||
|
||||
if go_string and go_string.num_liberties == 1:
|
||||
if go_string := game_state.board.get_go_string(point):
|
||||
num_atari_stones = min(len(go_string.stones), 8)
|
||||
board_tensor[offset("self_atari_size") + num_atari_stones, r, c] = 1
|
||||
|
||||
if is_ladder_capture(game_state, point):
|
||||
board_tensor[offset("ladder_capture"), r, c] = 1
|
||||
|
||||
if is_ladder_escape(game_state, point):
|
||||
board_tensor[offset("ladder_escape"), r, c] = 1
|
||||
|
||||
if self.use_player_plane:
|
||||
if game_state.next_player == Player.black:
|
||||
board_tensor[offset("ones")] = self.ones()
|
||||
else:
|
||||
board_tensor[offset("zeros")] = self.zeros()
|
||||
|
||||
return board_tensor
|
||||
|
||||
def ones(self):
|
||||
return torch.ones((1, self.board_height, self.board_width))
|
||||
|
||||
def zeros(self):
|
||||
return torch.zeros((1, self.board_height, self.board_width))
|
||||
|
||||
def capture_size(self, game_state, num_planes=8):
|
||||
pass
|
||||
|
||||
def encode_point(self, point):
|
||||
return self.board_width * (point.row - 1) + (point.col - 1)
|
||||
|
||||
def decode_point_index(self, index):
|
||||
row = index // self.board_width
|
||||
col = index % self.board_width
|
||||
return Point(row=row + 1, col=col + 1)
|
||||
|
||||
def num_points(self):
|
||||
return self.board_width * self.board_height
|
||||
|
||||
def shape(self):
|
||||
return self.num_planes, self.board_height, self.board_width
|
||||
|
||||
|
||||
def create(board_size):
|
||||
return AlphaGoEncoder(board_size)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import unittest
|
||||
|
||||
from tugo.agent.helpers import is_point_an_eye
|
||||
from tugo.goboard_fast import Board, GameState, Move
|
||||
from tugo.gotypes import Player, Point
|
||||
from tugo.encoders.alphago import AlphaGoEncoder
|
||||
|
||||
|
||||
class AlphaGoEncoderTest(unittest.TestCase):
|
||||
def test_encoder(self):
|
||||
alphago = AlphaGoEncoder()
|
||||
|
||||
start = GameState.new_game(19)
|
||||
next_state = start.apply_move(Move.play(Point(16, 16)))
|
||||
alphago.encode(next_state)
|
||||
|
||||
self.assertEquals(alphago.name(), 'alphago')
|
||||
self.assertEquals(alphago.board_height, 19)
|
||||
self.assertEquals(alphago.board_width, 19)
|
||||
self.assertEquals(alphago.num_planes, 49)
|
||||
self.assertEquals(alphago.shape(), (49, 19, 19))
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,51 @@
|
||||
# tag::importlib[]
|
||||
import importlib
|
||||
# end::importlib[]
|
||||
|
||||
__all__ = [
|
||||
'Encoder',
|
||||
'get_encoder_by_name',
|
||||
]
|
||||
|
||||
|
||||
# tag::base_encoder[]
|
||||
class Encoder:
|
||||
def name(self): # <1>
|
||||
raise NotImplementedError()
|
||||
|
||||
def encode(self, game_state): # <2>
|
||||
raise NotImplementedError()
|
||||
|
||||
def encode_point(self, point): # <3>
|
||||
raise NotImplementedError()
|
||||
|
||||
def decode_point_index(self, index): # <4>
|
||||
raise NotImplementedError()
|
||||
|
||||
def num_points(self): # <5>
|
||||
raise NotImplementedError()
|
||||
|
||||
def shape(self): # <6>
|
||||
raise NotImplementedError()
|
||||
|
||||
# <1> Lets us support logging or saving the name of the encoder our model is using.
|
||||
# <2> Turn a Go board into a numeric data.
|
||||
# <3> Turn a Go board point into an integer index.
|
||||
# <4> Turn an integer index back into a Go board point.
|
||||
# <5> Number of points on the board, i.e. board width times board height.
|
||||
# <6> Shape of the encoded board structure.
|
||||
# end::base_encoder[]
|
||||
|
||||
|
||||
# tag::encoder_by_name[]
|
||||
def get_encoder_by_name(name, board_size): # <1>
|
||||
if isinstance(board_size, int):
|
||||
board_size = (board_size, board_size) # <2>
|
||||
module = importlib.import_module('tugo.encoders.' + name)
|
||||
constructor = getattr(module, 'create') # <3>
|
||||
return constructor(board_size)
|
||||
|
||||
# <1> We can create encoder instances by referencing their name.
|
||||
# <2> If board_size is one integer, we create a square board from it.
|
||||
# <3> Each encoder implementation will have to provide a "create" function that provides an instance.
|
||||
# end::encoder_by_name[]
|
||||
@@ -0,0 +1,60 @@
|
||||
# tag::oneplane_imports[]
|
||||
import numpy as np
|
||||
|
||||
from tugo.encoders.base import Encoder
|
||||
from tugo.goboard import Point
|
||||
# end::oneplane_imports[]
|
||||
|
||||
|
||||
# tag::oneplane_encoder[]
|
||||
class OnePlaneEncoder(Encoder):
|
||||
def __init__(self, board_size):
|
||||
self.board_width, self.board_height = board_size
|
||||
self.num_planes = 1
|
||||
|
||||
def name(self): # <1>
|
||||
return 'oneplane'
|
||||
|
||||
def encode(self, game_state): # <2>
|
||||
board_matrix = np.zeros(self.shape())
|
||||
next_player = game_state.next_player
|
||||
for r in range(self.board_height):
|
||||
for c in range(self.board_width):
|
||||
p = Point(row=r + 1, col=c + 1)
|
||||
go_string = game_state.board.get_go_string(p)
|
||||
if go_string is None:
|
||||
continue
|
||||
if go_string.color == next_player:
|
||||
board_matrix[0, r, c] = 1
|
||||
else:
|
||||
board_matrix[0, r, c] = -1
|
||||
return board_matrix
|
||||
|
||||
# <1> We can reference this encoder by the name "oneplane".
|
||||
# <2> To encode, we fill a matrix with 1 if the point contains one of the current player's stones, -1 if the point contains the opponent's stones and 0 if the point is empty.
|
||||
# end::oneplane_encoder[]
|
||||
|
||||
# tag::oneplane_encoder_2[]
|
||||
def encode_point(self, point): # <1>
|
||||
return self.board_width * (point.row - 1) + (point.col - 1)
|
||||
|
||||
def decode_point_index(self, index): # <2>
|
||||
row = index // self.board_width
|
||||
col = index % self.board_width
|
||||
return Point(row=row + 1, col=col + 1)
|
||||
|
||||
def num_points(self):
|
||||
return self.board_width * self.board_height
|
||||
|
||||
def shape(self):
|
||||
return self.num_planes, self.board_height, self.board_width
|
||||
|
||||
# <1> Turn a board point into an integer index.
|
||||
# <2> Turn an integer index into a board point.
|
||||
# end::oneplane_encoder_2[]
|
||||
|
||||
|
||||
# tag::oneplane_create[]
|
||||
def create(board_size):
|
||||
return OnePlaneEncoder(board_size)
|
||||
# end::oneplane_create[]
|
||||
@@ -0,0 +1,57 @@
|
||||
# tag::sevenplane_init[]
|
||||
import numpy as np
|
||||
|
||||
from tugo.encoders.base import Encoder
|
||||
from tugo.goboard import Move, Point
|
||||
|
||||
|
||||
class SevenPlaneEncoder(Encoder):
|
||||
def __init__(self, board_size):
|
||||
self.board_width, self.board_height = board_size
|
||||
self.num_planes = 7
|
||||
|
||||
def name(self):
|
||||
return 'sevenplane'
|
||||
# end::sevenplane_init[]
|
||||
|
||||
# tag::sevenplane_encode[]
|
||||
def encode(self, game_state):
|
||||
board_tensor = np.zeros(self.shape())
|
||||
base_plane = {game_state.next_player: 0,
|
||||
game_state.next_player.other: 3}
|
||||
for row in range(self.board_height):
|
||||
for col in range(self.board_width):
|
||||
p = Point(row=row + 1, col=col + 1)
|
||||
go_string = game_state.board.get_go_string(p)
|
||||
if go_string is None:
|
||||
if game_state.does_move_violate_ko(game_state.next_player,
|
||||
Move.play(p)):
|
||||
board_tensor[6][row][col] = 1 # <1>
|
||||
else:
|
||||
liberty_plane = min(3, go_string.num_liberties) - 1
|
||||
liberty_plane += base_plane[go_string.color]
|
||||
board_tensor[liberty_plane][row][col] = 1 # <2>
|
||||
return board_tensor
|
||||
# <1> Encoding moves prohibited by the ko rule
|
||||
# <2> Encoding black and white stones with 1, 2 or more liberties.
|
||||
# end::sevenplane_encode[]
|
||||
|
||||
# tag::sevenplane_rest[]
|
||||
def encode_point(self, point):
|
||||
return self.board_width * (point.row - 1) + (point.col - 1)
|
||||
|
||||
def decode_point_index(self, index):
|
||||
row = index // self.board_width
|
||||
col = index % self.board_width
|
||||
return Point(row=row + 1, col=col + 1)
|
||||
|
||||
def num_points(self):
|
||||
return self.board_width * self.board_height
|
||||
|
||||
def shape(self):
|
||||
return self.num_planes, self.board_height, self.board_width
|
||||
|
||||
|
||||
def create(board_size):
|
||||
return SevenPlaneEncoder(board_size)
|
||||
# end::sevenplane_rest[]
|
||||
@@ -0,0 +1,67 @@
|
||||
import numpy as np
|
||||
|
||||
from tugo.encoders.base import Encoder
|
||||
from tugo.goboard import Move
|
||||
from tugo.gotypes import Player, Point
|
||||
|
||||
|
||||
class SimpleEncoder(Encoder):
|
||||
def __init__(self, board_size):
|
||||
"""
|
||||
Args:
|
||||
board_size: tuple of (width, height)
|
||||
"""
|
||||
self.board_width, self.board_height = board_size
|
||||
# 0 - 3. black stones with 1, 2, 3, 4+ liberties
|
||||
# 4 - 7. white stones with 1, 2, 3, 4+ liberties
|
||||
# 8. black plays next
|
||||
# 9. white plays next
|
||||
# 10. move would be illegal due to ko
|
||||
self.num_planes = 11
|
||||
|
||||
def name(self):
|
||||
return 'simple'
|
||||
|
||||
def encode(self, game_state):
|
||||
board_tensor = np.zeros(self.shape())
|
||||
if game_state.next_player == Player.black:
|
||||
board_tensor[8] = 1
|
||||
else:
|
||||
board_tensor[9] = 1
|
||||
for r in range(self.board_height):
|
||||
for c in range(self.board_width):
|
||||
p = Point(row=r + 1, col=c + 1)
|
||||
go_string = game_state.board.get_go_string(p)
|
||||
|
||||
if go_string is None:
|
||||
if game_state.does_move_violate_ko(game_state.next_player,
|
||||
Move.play(p)):
|
||||
board_tensor[10][r][c] = 1
|
||||
else:
|
||||
liberty_plane = min(4, go_string.num_liberties) - 1
|
||||
if go_string.color == Player.white:
|
||||
liberty_plane += 4
|
||||
board_tensor[liberty_plane][r][c] = 1
|
||||
|
||||
return board_tensor
|
||||
|
||||
def encode_point(self, point):
|
||||
"""Turn a board point into an integer index."""
|
||||
# Points are 1-indexed
|
||||
return self.board_width * (point.row - 1) + (point.col - 1)
|
||||
|
||||
def decode_point_index(self, index):
|
||||
"""Turn an integer index into a board point."""
|
||||
row = index // self.board_width
|
||||
col = index % self.board_width
|
||||
return Point(row=row + 1, col=col + 1)
|
||||
|
||||
def num_points(self):
|
||||
return self.board_width * self.board_height
|
||||
|
||||
def shape(self):
|
||||
return self.num_planes, self.board_height, self.board_width
|
||||
|
||||
|
||||
def create(board_size):
|
||||
return SimpleEncoder(board_size)
|
||||
@@ -0,0 +1,107 @@
|
||||
from tugo.goboard import Move
|
||||
|
||||
|
||||
def is_ladder_capture(game_state, candidate, recursion_depth=50):
|
||||
return is_ladder(True, game_state, candidate, None, recursion_depth)
|
||||
|
||||
|
||||
def is_ladder_escape(game_state, candidate, recursion_depth=50):
|
||||
return is_ladder(False, game_state, candidate, None, recursion_depth)
|
||||
|
||||
|
||||
def is_ladder(try_capture, game_state, candidate,
|
||||
ladder_stones=None, recursion_depth=50):
|
||||
"""Ladders are played out in reversed roles, one player tries to capture,
|
||||
the other to escape. We determine the ladder status by recursively calling
|
||||
is_ladder in opposite roles, providing suitable capture or escape candidates.
|
||||
|
||||
Arguments:
|
||||
try_capture: boolean flag to indicate if you want to capture or escape the ladder
|
||||
game_state: current game state, instance of GameState
|
||||
candidate: a move that potentially leads to escaping the ladder or capturing it, instance of Move
|
||||
ladder_stones: the stones to escape or capture, list of Point. Will be inferred if not provided.
|
||||
recursion_depth: when to stop recursively calling this function, integer valued.
|
||||
|
||||
Returns True if game state is a ladder and try_capture is true (the ladder captures)
|
||||
or if game state is not a ladder and try_capture is false (you can successfully escape)
|
||||
and False otherwise.
|
||||
"""
|
||||
|
||||
if not game_state.is_valid_move(Move(candidate)) or not recursion_depth:
|
||||
return False
|
||||
|
||||
next_player = game_state.next_player
|
||||
capture_player = next_player if try_capture else next_player.other
|
||||
escape_player = capture_player.other
|
||||
|
||||
if ladder_stones is None:
|
||||
ladder_stones = guess_ladder_stones(game_state, candidate, escape_player)
|
||||
|
||||
for ladder_stone in ladder_stones:
|
||||
current_state = game_state.apply_move(candidate)
|
||||
|
||||
if try_capture:
|
||||
candidates = determine_escape_candidates(
|
||||
game_state, ladder_stone, capture_player)
|
||||
attempted_escapes = [ # now try to escape
|
||||
is_ladder(False, current_state, escape_candidate,
|
||||
ladder_stone, recursion_depth - 1)
|
||||
for escape_candidate in candidates]
|
||||
|
||||
if not any(attempted_escapes):
|
||||
return True # if at least one escape fails, we capture
|
||||
else:
|
||||
if count_liberties(current_state, ladder_stone) >= 3:
|
||||
return True # successful escape
|
||||
if count_liberties(current_state, ladder_stone) == 1:
|
||||
continue # failed escape, others might still do
|
||||
candidates = liberties(current_state, ladder_stone)
|
||||
attempted_captures = [ # now try to capture
|
||||
is_ladder(True, current_state, capture_candidate,
|
||||
ladder_stone, recursion_depth - 1)
|
||||
for capture_candidate in candidates]
|
||||
if any(attempted_captures):
|
||||
continue # failed escape, try others
|
||||
return True # candidate can't be caught in a ladder, escape.
|
||||
return False # no captures / no escapes
|
||||
|
||||
|
||||
def is_candidate(game_state, move, player):
|
||||
return game_state.next_player == player and \
|
||||
count_liberties(game_state, move) == 2
|
||||
|
||||
|
||||
def guess_ladder_stones(game_state, move, escape_player):
|
||||
adjacent_strings = [game_state.board.get_go_string(nb) for nb in move.neighbors() if game_state.board.get_go_string(nb)]
|
||||
if adjacent_strings:
|
||||
string = adjacent_strings[0]
|
||||
neighbors = []
|
||||
for string in adjacent_strings:
|
||||
stones = string.stones
|
||||
for stone in stones:
|
||||
neighbors.append(stone)
|
||||
return [Move(nb) for nb in neighbors if is_candidate(game_state, Move(nb), escape_player)]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def determine_escape_candidates(game_state, move, capture_player):
|
||||
escape_candidates = move.neighbors()
|
||||
for other_ladder_stone in game_state.board.get_go_string(move).stones:
|
||||
for neighbor in other_ladder_stone.neighbors():
|
||||
right_color = game_state.color(neighbor) == capture_player
|
||||
one_liberty = count_liberties(game_state, neighbor) == 1
|
||||
if right_color and one_liberty:
|
||||
escape_candidates.append(liberties(game_state, neighbor))
|
||||
return escape_candidates
|
||||
|
||||
|
||||
def count_liberties(game_state, move):
|
||||
if game_state.board.get_go_string(move):
|
||||
return game_state.board.get_go_string(move).num_liberties
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def liberties(game_state, move):
|
||||
return list(game_state.board.get_go_string(move).liberties)
|
||||
Reference in New Issue
Block a user