Skip to content

Commit

Permalink
Adding logger for AI, AI running, still some bugs with util of discards
Browse files Browse the repository at this point in the history
  • Loading branch information
Pontiffx committed Feb 25, 2010
1 parent 6201595 commit 5931471
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 82 deletions.
7 changes: 6 additions & 1 deletion conf/logging.conf
@@ -1,5 +1,5 @@
[loggers] [loggers]
keys=root,snm.controller,snm.view,snm.model,snm.cardview keys=root,snm.controller,snm.view,snm.model,snm.cardview,snm.agent


[handlers] [handlers]
keys=consoleHandler keys=consoleHandler
Expand All @@ -23,6 +23,11 @@ handlers=consoleHandler
qualname=snm.view qualname=snm.view
propagate=0 propagate=0


[logger_snm.agent]
level=INFO
handlers=consoleHandler
qualname=snm.agent
propagate=0


[logger_snm.model] [logger_snm.model]
level=INFO level=INFO
Expand Down
104 changes: 60 additions & 44 deletions lib/agent.py
Expand Up @@ -3,20 +3,21 @@
""" """


from model import * from model import *
from view import Player from player import Player
import sys import sys
import random import random
from copy import copy, deepcopy from copy import copy, deepcopy
from cardmodels import Card from cardmodels import Card
from time import sleep
import logging import logging


log = logging.getLogger("snm.agent") log = logging.getLogger("snm.agent")




class StateNode(object): class StateNode(object):
" A node in the search that represents a current state of the board " " A node in the search that represents a current state of the board "
SELF = 1 SELF = "self"
OTHER = 2 OTHER = "other"


def __init__(self, state, action=None, parent_node=None): def __init__(self, state, action=None, parent_node=None):
self.state = state self.state = state
Expand All @@ -37,18 +38,22 @@ def __eq__(self, other):
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)


def __str__(self):
return "Node[%d](%s,%s,child_nodes:%d,player:%s)" % (
self.util_value, self.state, self.action, len(self.child_nodes), self.player)



class ComputerPlayer(Player): class ComputerPlayer(Player):
""" """
An AI player for Spite and Malice. This uses a modified version of minimax that An AI player for Spite and Malice. This uses a modified version of minimax that
checks each state to see which player is player, and evaluates accordingly. checks each state to see which player is player, and evaluates accordingly.
""" """


MIN_VALUE = -sys.max_int MIN_VALUE = -sys.maxint
MAX_VALUE = sys.maxint MAX_VALUE = sys.maxint


def __init__(self, model) def __init__(self):
" setup the ai ignoring the model, so it can't cheat " " setup the ai "
# list of moves stored up # list of moves stored up
self.play_queue = [] self.play_queue = []


Expand All @@ -60,21 +65,22 @@ def play_card(self, game_state):
""" """
# play queued moves if we have some # play queued moves if we have some
if len(self.play_queue) > 0: if len(self.play_queue) > 0:
sleep(0.3)
return self.play_queue.pop(0) return self.play_queue.pop(0)


# find the best possible move # find the best possible move
self.terminal_nodes = [] self.terminal_nodes = []
node = StateNode(game_state) node = StateNode(game_state)
self._evaluate(node) self._evaluate(node)


self._build_play_queue(terminal_nodes) self._build_play_queue()
return self.play_queue.pop() return self.play_queue.pop(0)




def _evaluate(self, node): def _evaluate(self, node):
" Evaluate a node, and recurse if necessary " " Evaluate a node, and recurse if necessary "
# no reason to get util for starting state # no reason to get util for starting state
if node.parent: if node.parent_node:
node.util_value = self._utility(node) node.util_value = self._utility(node)


if self._terminal_test(node): if self._terminal_test(node):
Expand Down Expand Up @@ -137,17 +143,19 @@ def _successors(self, node):
node.parent_node.action.from_pile != DISCARD): node.parent_node.action.from_pile != DISCARD):


# moves to center # moves to center
for pile_name, pile_len in [(HAND,1), (PAYOFF,1), (DISCARD,4)]: for pile_name, pile_len in [(HAND,1), (PAY_OFF,1), (DISCARD,4)]:
for id in range(pile_len): for pile_id in range(pile_len):
for action in self._get_center_move_from(node.state, pile_name, id): for action in self._get_center_move_from(node.state, pile_name, pile_id):
new_state = ComputerPlayer._new_state_from_action(node.state, action) new_state = ComputerPlayer._new_state_from_action(node.state, action)
new_node = StateNode(new_state, action, node) new_node = StateNode(new_state, action, node)
if new_node not in node_list: if new_node not in node_list:
node_list.append(new_node) node_list.append(new_node)


# moves to discard # moves to discard
for card in node.state.get_player()[HAND] for card in node.state.get_player()[HAND]:
for pile_id in len(node.state.get_player()[DISCARD]): for pile_id in range(len(node.state.get_player()[DISCARD])):
if Card.to_numeric_value(card) == 13:
continue
action = PlayerMove(card, from_pile=HAND, to_pile=DISCARD, to_id=pile_id) action = PlayerMove(card, from_pile=HAND, to_pile=DISCARD, to_id=pile_id)
new_state = ComputerPlayer._new_state_from_action(node.state, action) new_state = ComputerPlayer._new_state_from_action(node.state, action)
new_node = StateNode(new_state, action, node) new_node = StateNode(new_state, action, node)
Expand All @@ -158,23 +166,27 @@ def _successors(self, node):


# TODO: assume apponent will always play pay_off if they can, do i need min/max ? # TODO: assume apponent will always play pay_off if they can, do i need min/max ?
# opponent plays card on center # opponent plays card on center
for pile_name, pile_len in [(PAYOFF,1), (DISCARD,4)]: for pile_name, pile_len in [(PAY_OFF,1), (DISCARD,4)]:
for id in range(pile_len): for pile_id in range(pile_len):
for action in self._get_center_move_from(node.state, pile_name, id, True): for action in self._get_center_move_from(node.state, pile_name, pile_id, True):
new_state = ComputerPlayer._new_state_from_action(node.state, action, True) new_state = ComputerPlayer._new_state_from_action(node.state, action, True)
new_node = StateNode(new_state, action, node) new_node = StateNode(new_state, action, node)
if new_node not in node_list: if new_node not in node_list:
node_list.append(new_node) node_list.append(new_node)




@staticmethod @staticmethod
def _get_center_move_from(state, pile_name, pile_index, new_player=False): def _get_center_move_from(state, pile_name, pile_index, other_player=False):
" Get all the valid center stack placements from a pile " " Get all the valid center stack placements from a pile "
moves = [] moves = []


pile = state.get_player(new_player)[pile_name] # set pile to HAND or PAY_OFF stacks
if pile_name == DISCARD and not new_player: pile = state.get_player(other_player)[pile_name]
pile = [state.get_player(new_player)[pile_name][id][-1]] # otherwise, set it to top of DISCARD
if pile_name == DISCARD:
if len(state.get_player(other_player)[pile_name][pile_index]) < 1:
return moves
pile = [state.get_player(other_player)[pile_name][pile_index][-1]]


for i in range(len(pile)): for i in range(len(pile)):
card = pile[i] card = pile[i]
Expand Down Expand Up @@ -210,6 +222,7 @@ def _utility(self, node):
-3 Each card in hand -3 Each card in hand
-80 Opponent plays pay_off card -80 Opponent plays pay_off card
""" """
# TODO: small value for reducing size of discard, if it does not bring opponent closer
# shortcut vars # shortcut vars
center_values = [] center_values = []
for pile in node.state.center_stacks: for pile in node.state.center_stacks:
Expand All @@ -234,7 +247,7 @@ def _utility(self, node):
value += 120 value += 120
# each time the discard cards value ocurs in the hand # each time the discard cards value ocurs in the hand
if node.action.to_pile == DISCARD: if node.action.to_pile == DISCARD:
value += 10 * map(lambda c: Card.to_numeric_value(c), myself[HARD]).count( value += 10 * map(lambda c: Card.to_numeric_value(c), myself[HAND]).count(
Card.to_numeric_value(node.action.card)) Card.to_numeric_value(node.action.card))
# discard on same value card # discard on same value card
if node.action.to_pile == DISCARD and \ if node.action.to_pile == DISCARD and \
Expand All @@ -257,13 +270,13 @@ def _utility(self, node):
discard_piles, myself[PAY_OFF][-1]): discard_piles, myself[PAY_OFF][-1]):
value += 2 value += 2
# discard least essential card # discard least essential card
if node.action.to_pile == DISCARD and 0 == self.find_least_essential_card( if node.action.to_pile == DISCARD and 0 == self._find_least_essential_card(
center_values, [node.action.card] + myself[HAND], myself[PAY_OFF][-1]): center_values, [node.action.card] + myself[HAND], myself[PAY_OFF][-1]):
value += 1 value += 1


# each point away the closest center is from opponents pay off # each point away the closest center is from opponents pay off
value += 4 * self._distance_between_values(self._find_closest_center_stack_value( value += 4 * self._distance_between_values(self._find_closest_center_stack_value(
center_values, other[PAY_OFF][-1])) center_values, other[PAY_OFF][-1]), other[PAY_OFF][-1])


# each card in hand # each card in hand
value -= 3 * len(myself[HAND]) value -= 3 * len(myself[HAND])
Expand All @@ -283,10 +296,11 @@ def _find_least_essential_card(center_values, pile, pay_off_card):
Return the id in pile that is considered least essential relative to the Return the id in pile that is considered least essential relative to the
pay off card. pay off card.
""" """
center_value = self._find_closest_center_stack_value(center_values, pay_off_card) center_value = ComputerPlayer._find_closest_center_stack_value(center_values, pay_off_card)
list_values = map(lambda c: self._distance_between_values(c, pay_off_card), center_values) list_values = map(lambda c: ComputerPlayer._distance_between_values(
center_value, Card.to_numeric_value(c)), pile)


return list_values.index(min(list_values)) return list_values.index(max(list_values))




@staticmethod @staticmethod
Expand All @@ -296,7 +310,7 @@ def _find_closest_center_stack_value(center_stacks, pay_off_card):
for playing the pay_off_card. for playing the pay_off_card.
""" """
po_value = Card.to_numeric_value(pay_off_card) po_value = Card.to_numeric_value(pay_off_card)
return min(map(lambda c: self._distance_between(Card.to_numeric_value(c), po_value), center_stacks)) return min(map(lambda c: ComputerPlayer._distance_between_values(Card.to_numeric_value(c), po_value), center_stacks))
# distance = ComputerPlayer.MAX_VALUE # distance = ComputerPlayer.MAX_VALUE
# closest_value = None # closest_value = None
# for stack_id in range(len(center_stacks)): # for stack_id in range(len(center_stacks)):
Expand All @@ -319,25 +333,27 @@ def _distance_between_values(pile_card, play_card):
return 12 return 12
return 12 - pile_card + play_card return 12 - pile_card + play_card


def _build_play_queue(self, node) def _build_play_queue(self):
""" """
Given a node, build the play queue from the child node with the given Find the best path, and build the play queue for this path.
value to the discard move, and store it. If moves share a util In the case of a tie, pick a random path
value (somehow), chose a random node from them.
""" """
node = max(self.terminal_nodes, key=lambda s: s.util_value)

chain = []
# loop while there are still nodes in the chain # loop while there are still nodes in the chain
# TODO: fix this for new code
while node: while node:
possible_paths = [] # skip any opponent nodes we have in the chain
# for each child, find the path with the desired value if node.player == StateNode.OTHER:
for child in node.child_nodes: node = node.parent_node
if child.util_value == value: continue
possible_paths.append(child) chain.append(node.action)
if len(possible_paths) == 0: node = node.parent_node
return
node = random.choice(possible_paths) # remove the starting state, and reverse the list, so we can traverse the path
self.play_queue.append(node.state) chain.pop()
if node.state[2][0] == DISCARD: chain.reverse()
return log.info("Chosing path: %s" % (" ".join(map(str, chain))))
self.play_queue = chain




86 changes: 53 additions & 33 deletions lib/model.py
Expand Up @@ -65,44 +65,12 @@ def __eq__(self, other):
self.to_id == other.to_id: self.to_id == other.to_id:
return True return True
return False return False
return False


def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)






class GameState(SpiteAndMaliceModel):
" A copy of the visible game state for a player "
def __init__(self, game):
self.active_player = game.active_player
self.players = [{}, {}]

# copy of the visible cards for player
a_id = self.active_player
self.players[a_id][HAND] = deepcopy(game.players[a_id][HAND])
self.players[a_id][PAY_OFF] = [game.players[a_id][PAY_OFF].visible()[-1]]
self.players[a_id][DISCARD] = deepcopy(game.players[a_id][DISCARD])

# copy of visible cards of his opponent
o_id = int(not a_id)
self.players[o_id][PAY_OFF] = [game.players[o_id][PAY_OFF].visible()[-1]]
self.players[o_id][DISCARD] = deepcopy(game.players[o_id][DISCARD])

def __eq__(self, other):
if other == None or type(other) != GameState:
return False
for pile in self.players[0].keys():
if self.players[0][pile] != other.players[0][pile]:
return False
return True

def __ne__(self, other):
return not self.__eq__(other)

#TODO: make other functions not callable


class SpiteAndMaliceModel(object): class SpiteAndMaliceModel(object):


NUM_STACKS = 4 NUM_STACKS = 4
Expand Down Expand Up @@ -149,10 +117,11 @@ def get_player(self, other=False):
" Return a players cards " " Return a players cards "
if not other: if not other:
return self.players[self.active_player] return self.players[self.active_player]
return self.players[int(not self.active_player] return self.players[int(not self.active_player)]


def build_view_for_player(self): def build_view_for_player(self):
" Return a copy of the current view as a GameState object for the player. " " Return a copy of the current view as a GameState object for the player. "
return GameState(self)




@classmethod @classmethod
Expand Down Expand Up @@ -247,3 +216,54 @@ def is_won(self):
return (len(self.players[self.active_player][PAY_OFF]) == 0) return (len(self.players[self.active_player][PAY_OFF]) == 0)





class GameState(SpiteAndMaliceModel):
" A copy of the visible game state for a player "
def __init__(self, game):
self.active_player = game.active_player
self.players = [{}, {}]

# copy of the visible cards for player
a_id = self.active_player
self.players[a_id][HAND] = deepcopy(game.players[a_id][HAND])
self.players[a_id][PAY_OFF] = [game.players[a_id][PAY_OFF].visible()[-1]]
self.players[a_id][DISCARD] = deepcopy(game.players[a_id][DISCARD])

# copy of visible cards of his opponent
o_id = int(not a_id)
self.players[o_id][PAY_OFF] = [game.players[o_id][PAY_OFF].visible()[-1]]
self.players[o_id][DISCARD] = deepcopy(game.players[o_id][DISCARD])

# center stacks
self.center_stacks = deepcopy(game.center_stacks)

def __eq__(self, other):
if other == None or type(other) != GameState:
return False
for pile in self.players[0].keys():
if self.players[0][pile] != other.players[0][pile]:
return False
return True

def __ne__(self, other):
return not self.__eq__(other)

def __str__(self):
s = self.get_player()
o = self.get_player(True)
sd = []
od = []
cs = []
for p, pd in ((s[DISCARD],sd), (o[DISCARD],od), (self.center_stacks, cs)):
for pile in p:
if len(pile) > 0:
pd.append(pile[-1])
else:
pd.append('')
return "State([%s]hand[%s]disc[%s],[%s]disc[%s],center[%s])" % (
s[PAY_OFF][-1], ''.join(s[HAND]), ''.join(sd), o[PAY_OFF][-1], ''.join(od),
''.join(cs))

#TODO: make other functions not callable


2 changes: 1 addition & 1 deletion lib/player.py
Expand Up @@ -13,7 +13,7 @@
class Player(object): class Player(object):
" interface definition for a player of Spite and Malice " " interface definition for a player of Spite and Malice "


def play_card(self, my_cards, opponents_cards, center_stacks): def play_card(self, *args):
""" """
This method is called when it is time for the player to place a card. This method is called when it is time for the player to place a card.
The player is given the two maps and a list of center stacks. The first map, The player is given the two maps and a list of center stacks. The first map,
Expand Down

0 comments on commit 5931471

Please sign in to comment.