This commit is contained in:
2023-05-23 15:52:09 +08:00
parent e8f9c8a287
commit 663edbb5e2
88 changed files with 6250 additions and 0 deletions
+6
View File
@@ -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 *
+141
View File
@@ -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)
+26
View File
@@ -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()
+51
View File
@@ -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[]
+60
View File
@@ -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[]
+57
View File
@@ -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[]
+67
View File
@@ -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)
+107
View File
@@ -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)