Example #1
0
 def __init__(self, colour, starting):
     if colour not in players:
         raise ValueError('illegal colour {}'.format(colour))
     self.colour = colour
     self.starting = starting
     self.verts = Verts([])
     self.cards = []
     self.rescards = []
Example #2
0
 def __init__(self, tiles):
     """ tiles is a list of legal tiles
     """
     # automatically handle if is already Tiles type
     self.tiles = Tiles(tiles)
     # initialise with an empty set of vertices
     self.verts = Verts([Vertex(x, y) for x, y in legal_verts])
     self.total_pips = {res: sum(tile.pips for tile in self.tiles.resource(res)) for res in resources}
Example #3
0
class Player:
    def __init__(self, colour, starting):
        if colour not in players:
            raise ValueError('illegal colour {}'.format(colour))
        self.colour = colour
        self.starting = starting
        self.verts = Verts([])
        self.cards = []
        self.rescards = []

    def settle(self, vertex):
        self.verts.append(vertex)

    def pickup(self, card):
        self.cards.append(card)
Example #4
0
 def tv(self, x, y):
     # always 6
     adj = [
         self.verts[x, y], self.verts[x + 1, y], self.verts[x + 2, y],
         self.verts[x, y + 1], self.verts[x + 1, y + 1], self.verts[x + 2,
                                                                    y + 1]
     ]
     return Verts(adj)
Example #5
0
 def vv(self, x, y):
     # if sum is even, go up
     # if sum is odd, go down
     if (x + y) % 2 == 0:
         adj = [
             self.verts[x, y + 1], self.verts[x - 1, y], self.verts[x + 1,
                                                                    y]
         ]
     else:
         adj = [
             self.verts[x, y - 1], self.verts[x - 1, y], self.verts[x + 1,
                                                                    y]
         ]
     return Verts(adj)
Example #6
0
class Board:
    """A container for vertices and tiles

    Contains board state (what has been settled) and functions to calculate worth of a vertex.

    a standard board has...
        19 tiles
            3 rock
            3 clay
            4 sheep
            4 wood
            4 wheat
            1 desert
        54 vertices
    """
    def __init__(self, tiles):
        """ tiles is a list of legal tiles
        """
        # automatically handle if is already Tiles type
        self.tiles = Tiles(tiles)
        # initialise with an empty set of vertices
        self.verts = Verts([Vertex(x, y) for x, y in legal_verts])
        self.total_pips = {res: sum(tile.pips for tile in self.tiles.resource(res)) for res in resources}

    @classmethod
    def random(cls):
        """Randomly create a board
           Do not allow a 6 and 8 to be adjacent
        """
        is_bad = True
        while is_bad:
            brd = cls(random_tiles())
            is_bad = brd._six_eight_adjacent()
        return brd

    def _six_eight_adjacent(self):
        """Are any 6 or 8 tiles next to each other?
        """
        for tile in self.tiles.roll(6):
            for t2 in self.tt(tile.x, tile.y):
                if t2.roll == 8 or t2.roll == 6:
                    return True
        for tile in self.tiles.roll(8):
            for t2 in self.tt(tile.x, tile.y):
                if t2.roll == 8 or t2.roll == 6:
                    return True

        return False

    def resource_table(self):
        """A DataFrame summarising the board
        """
        table = pd.DataFrame({'expected_pips': expected_pips,
                              'total_pips': self.total_pips})
        table['more_than_expec'] = table['total_pips'] / table['expected_pips']
        return table

    # actions
    def settle(self, x, y, player):
        self.verts[x, y].settle(player)
        for vert in self.vv(x, y):
            vert.block()

    def unsettle(self, x, y):
        # unsettle and unblock
        self.verts[x, y].unsettle()
        for vert in self.vv(x, y):
            vert.unblock()

        # re-settle all vertices in order to check blocking is correct
        for vert in self.verts:
            if vert.settled:
                self.settle(vert.x, vert.y, vert.settled)


    # get adjacent items
    def tt(self, x, y):
        adj = [
            self.tiles[x+2, y],
            self.tiles[x-2, y],
            self.tiles[x-1, y+1],
            self.tiles[x+1, y+1],
            self.tiles[x-1, y-1],
            self.tiles[x+1, y-1]
        ]
        return Tiles(adj)

    def tv(self, x, y):
        # always 6
        adj = [
            self.verts[x, y],
            self.verts[x+1, y],
            self.verts[x+2, y],
            self.verts[x, y+1],
            self.verts[x+1, y+1],
            self.verts[x+2, y+1]
        ]
        return Verts(adj)

    def vv(self, x, y):
        # if sum is even, go up
        # if sum is odd, go down
        if (x+y)%2 == 0:
            adj = [
                self.verts[x, y+1],
                self.verts[x-1, y],
                self.verts[x+1, y]
            ]
        else:
            adj = [
                self.verts[x, y-1],
                self.verts[x-1, y],
                self.verts[x+1, y]
            ]
        return Verts(adj)

    def vt(self, x, y):
        if (x+y)%2 == 0:
            adj = [
                self.tiles[x, y],
                self.tiles[x-2, y],
                self.tiles[x-1, y-1]
            ]
        else:
            adj = [
                self.tiles[x-1, y],
                self.tiles[x, y-1],
                self.tiles[x-2, y-1]
            ]
        return Tiles(adj)


    # metrics- return the value for a particular vertex
    metrics = ['pips', 'relpips', 'pipworth', 'ave_potential', 'blocking']

    def pips(self, x, y, *_):
        """Total adjacent pips to vertex
        """
        return sum(tile.pips for tile in self.vt(x, y))

    def relpips(self, x, y, *_):
        """Total worth of adjacent resources to vertex, considering resource scarcity and
           exogenous weights
        """
        worth = 0
        for tile in self.vt(x, y):
            res = tile.resource
            if res == 'desert':
                continue
            worth += tile.pips*resource_weighting[res]/self.total_pips[res]
        # scale worth to be on the same scale as regular pips. this is the average expected pips
        return worth*(58/5)

    def pipworth(self, x, y, player=None):
        """Total worth of adjacent resources to vertex, considering resource scarcity,
           exogenous weights and players current settlements
        """
        # a mapping from resource type to a list of worths
        pipmap = defaultdict(list)

        for tile in self.vt(x, y):
            res = tile.resource
            if res == 'desert':
                continue
            pipmap[res].append(tile.pips*resource_weighting[res]/self.total_pips[res])

        # Player state adjustment
        # add in a 0 pip placeholder for all already settled spots- lets us decay all subsequent resources
        # there is quite a bit of duplication here, because, for a certain player this code
        # will get repeated a lot. solution is maybe to pass a pipmap in and calculate under best()?
        for vert in self.verts.player(player):
            for t in self.vt(x, y):
                pipmap[t.resource].append(0)

        # decay the higher value or later resources by more
        worth = 0
        for res, piplist in pipmap.items():
            worth += np.dot(sorted(piplist), resdecay[:len(piplist)])

        # roll diversity??
        return worth*(58/5)

    def ave_potential(self, x, y, player=None):
        """The maximum average potential score, considering the (up to 3) roads a player may
           build from a settlement
        """
        return max(self.all_potentials(x, y, player).values())

    def all_potentials(self, x, y, player=None):
        """Considers all potential settlement locations
           returns a dictionary of x,y: pips, representing
           up to 3 vertex locations 1 step away from x,y.
           pips is the sum of the locations one road away from this vertex
        """
        vert_worth = {}
        for vert in self.vv(x, y):
            worth = 0
            for vert2 in self.vv(vert.x, vert.y):
                if (vert2.x, vert2.y) == (x, y):
                    continue
                worth += self.pipworth(vert2.x, vert2.y, player)
            vert_worth[vert.x, vert.y] = worth/2
        return vert_worth

    def blocking(self, x, y, player=None):
        """Blocking score- the average of the pipworth of the 3 surrounding vertices,
           which may not be settled if a settlement if placed here

        TODO: this should maybe be considered as blocking other's pairs- eg. a player really needs rock, so this is a good block
        """
        pips = 0
        for vert in self.vv(x, y).nonblocked():
            pips += self.pipworth(vert.x, vert.y, player)
        return pips/3


    # get all metrics
    def worths(self, method='pips', player=None):
        """Given a method, calculate the worth of every nonblocked vertex
        """
        worths = {}
        for vert in self.verts.nonblocked():
            x, y = vert.x, vert.y
            worths[x, y] = getattr(self, method)(x, y, player)
        return worths

    def best(self, sort_method='relpips', player=None):
        """Return a sorted dataframe of vertex worths (for a particular player)
        """
        values = [pd.Series(self.worths(m, player), name=m) for m in self.metrics]
        df = pd.concat(values, axis=1)
        df.index.names = ['x', 'y']
        df['total'] = df.sum('columns')
        df['tiles'] = df.index.map(lambda pos: self.vt(*pos).short_print())
        return df.sort_values(sort_method, ascending=False)

    def best_pair(self, method='relpips', player=None):
        """The best two positions for a player at this point in time
           This will implicitly take into account the fact that two of the same resource is worth less
        """
        pairs = {}
        ranked = self.best(player=player)
        for (x, y), first_vert_worth in ranked.iterrows():
            # fake settling this location and find the other best location
            self.settle(x, y, player=player)
            best_pair = self.best(player=player).reset_index().iloc[0]
            pairs[x, y, best_pair['x'], best_pair['y']] = first_vert_worth[method] + best_pair[method]
            self.unsettle(x, y)

        df = pd.Series(pairs)
        df.index.names = ['x1', 'y1', 'x2', 'y2']
        return df.to_frame(method).sort_values(method, ascending=False)


    # def b2(self, method='relpips', player=None, player_c):
    #     """Return a dataframe of best positions for the second to last placement (B2 in a A1 B1 C1 C2 B2 A2 scheme)
    #     """
    #     worths = {}
    #     for vert in self.verts.nonblocked():
    #         x, y = vert.x, vert.y
    #         # pretend to settle, then simulate next players actions
    #         self.settle(x, y, player=player)
    #         # NEST HERE for next players action afterwards:
    #         # for vert in self.verts.nonblocked():
    #         best_pair = self.best(player=player_c).reset_index().iloc[0]  # TODO we don't need to calculate for every method, which is faster
    #         self.settle(best_pair[x], best_pair[y], player_c)  # TODO we actually don't need this to block, which is faster to unsettle again

    #         # get the worth of this position, knowing the next players action
    #         self.unsettle(x, y)
    #         worths[x, y] = getattr(self, method)(x, y, player)  # self.pipworth(x, y, player=player)
            
    #         # unsettle next players best action
    #         self.unsettle(best_pair[x], best_pair[y])
        
    #     df = pd.Series(worths)
    #     df.index.names = ['x', 'y']
    #     return df.to_frame(method).sort_values(method, ascending=False)


    # plotting
    def plot(self):
        fig, ax = plt.subplots(figsize=(10,10))
        plt.axis('off')
        # Ocean
        # octagon = ([0.7, -0.3], [4.2, -0.3], [5.5, 2.5], [4.2, 5.2], [0.7, 5.2], [-0.5, 2.5])
        # ax.add_patch(patches.Polygon(octagon, color='blue'))

        self.tiles.plot(ax)
        self.verts.plot(ax)
    

    def plot_best(self, method='total', n=5):
        for i, ((x, y), row) in enumerate(self.best(method).head(n).iterrows(), start=1):
            x, y = Vertex(x, y).real_xy
            plt.plot(x, y, marker='x', color='purple', markeredgewidth=3)
            plt.text(x+0.1, y, i, color='purple', weight='bold')