Skip to content
Browse files

Adding logger for AI, AI running, still some bugs with util of discards

  • Loading branch information...
1 parent 6201595 commit 5931471789513424466cb5531c0ced8cdd31af87 Pontiffx committed Feb 25, 2010
Showing with 123 additions and 82 deletions.
  1. +6 −1 conf/logging.conf
  2. +60 −44 lib/agent.py
  3. +53 −33 lib/model.py
  4. +1 −1 lib/player.py
  5. +2 −2 lib/snm.py
  6. +1 −1 snm
View
7 conf/logging.conf
@@ -1,5 +1,5 @@
[loggers]
-keys=root,snm.controller,snm.view,snm.model,snm.cardview
+keys=root,snm.controller,snm.view,snm.model,snm.cardview,snm.agent
[handlers]
keys=consoleHandler
@@ -23,6 +23,11 @@ handlers=consoleHandler
qualname=snm.view
propagate=0
+[logger_snm.agent]
+level=INFO
+handlers=consoleHandler
+qualname=snm.agent
+propagate=0
[logger_snm.model]
level=INFO
View
104 lib/agent.py
@@ -3,20 +3,21 @@
"""
from model import *
-from view import Player
+from player import Player
import sys
import random
from copy import copy, deepcopy
from cardmodels import Card
+from time import sleep
import logging
log = logging.getLogger("snm.agent")
class StateNode(object):
" A node in the search that represents a current state of the board "
- SELF = 1
- OTHER = 2
+ SELF = "self"
+ OTHER = "other"
def __init__(self, state, action=None, parent_node=None):
self.state = state
@@ -37,18 +38,22 @@ def __eq__(self, other):
def __ne__(self, 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):
"""
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.
"""
- MIN_VALUE = -sys.max_int
+ MIN_VALUE = -sys.maxint
MAX_VALUE = sys.maxint
- def __init__(self, model)
- " setup the ai ignoring the model, so it can't cheat "
+ def __init__(self):
+ " setup the ai "
# list of moves stored up
self.play_queue = []
@@ -60,21 +65,22 @@ def play_card(self, game_state):
"""
# play queued moves if we have some
if len(self.play_queue) > 0:
+ sleep(0.3)
return self.play_queue.pop(0)
# find the best possible move
self.terminal_nodes = []
node = StateNode(game_state)
self._evaluate(node)
- self._build_play_queue(terminal_nodes)
- return self.play_queue.pop()
+ self._build_play_queue()
+ return self.play_queue.pop(0)
def _evaluate(self, node):
" Evaluate a node, and recurse if necessary "
# no reason to get util for starting state
- if node.parent:
+ if node.parent_node:
node.util_value = self._utility(node)
if self._terminal_test(node):
@@ -137,17 +143,19 @@ def _successors(self, node):
node.parent_node.action.from_pile != DISCARD):
# moves to center
- for pile_name, pile_len in [(HAND,1), (PAYOFF,1), (DISCARD,4)]:
- for id in range(pile_len):
- for action in self._get_center_move_from(node.state, pile_name, id):
+ for pile_name, pile_len in [(HAND,1), (PAY_OFF,1), (DISCARD,4)]:
+ for pile_id in range(pile_len):
+ 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_node = StateNode(new_state, action, node)
if new_node not in node_list:
node_list.append(new_node)
# moves to discard
- for card in node.state.get_player()[HAND]
- for pile_id in len(node.state.get_player()[DISCARD]):
+ for card in node.state.get_player()[HAND]:
+ 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)
new_state = ComputerPlayer._new_state_from_action(node.state, action)
new_node = StateNode(new_state, action, node)
@@ -158,23 +166,27 @@ def _successors(self, node):
# TODO: assume apponent will always play pay_off if they can, do i need min/max ?
# opponent plays card on center
- for pile_name, pile_len in [(PAYOFF,1), (DISCARD,4)]:
- for id in range(pile_len):
- for action in self._get_center_move_from(node.state, pile_name, id, True):
+ for pile_name, pile_len in [(PAY_OFF,1), (DISCARD,4)]:
+ for pile_id in range(pile_len):
+ 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_node = StateNode(new_state, action, node)
if new_node not in node_list:
node_list.append(new_node)
@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 "
moves = []
- pile = state.get_player(new_player)[pile_name]
- if pile_name == DISCARD and not new_player:
- pile = [state.get_player(new_player)[pile_name][id][-1]]
+ # set pile to HAND or PAY_OFF stacks
+ pile = state.get_player(other_player)[pile_name]
+ # 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)):
card = pile[i]
@@ -210,6 +222,7 @@ def _utility(self, node):
-3 Each card in hand
-80 Opponent plays pay_off card
"""
+ # TODO: small value for reducing size of discard, if it does not bring opponent closer
# shortcut vars
center_values = []
for pile in node.state.center_stacks:
@@ -234,7 +247,7 @@ def _utility(self, node):
value += 120
# each time the discard cards value ocurs in the hand
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))
# discard on same value card
if node.action.to_pile == DISCARD and \
@@ -257,13 +270,13 @@ def _utility(self, node):
discard_piles, myself[PAY_OFF][-1]):
value += 2
# 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]):
value += 1
# each point away the closest center is from opponents pay off
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
value -= 3 * len(myself[HAND])
@@ -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
pay off card.
"""
- center_value = self._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)
+ center_value = ComputerPlayer._find_closest_center_stack_value(center_values, pay_off_card)
+ 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
@@ -296,7 +310,7 @@ def _find_closest_center_stack_value(center_stacks, pay_off_card):
for playing the 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
# closest_value = None
# for stack_id in range(len(center_stacks)):
@@ -319,25 +333,27 @@ def _distance_between_values(pile_card, play_card):
return 12
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
- value to the discard move, and store it. If moves share a util
- value (somehow), chose a random node from them.
+ Find the best path, and build the play queue for this path.
+ In the case of a tie, pick a random path
"""
+ node = max(self.terminal_nodes, key=lambda s: s.util_value)
+
+ chain = []
# loop while there are still nodes in the chain
- # TODO: fix this for new code
while node:
- possible_paths = []
- # for each child, find the path with the desired value
- for child in node.child_nodes:
- if child.util_value == value:
- possible_paths.append(child)
- if len(possible_paths) == 0:
- return
- node = random.choice(possible_paths)
- self.play_queue.append(node.state)
- if node.state[2][0] == DISCARD:
- return
+ # skip any opponent nodes we have in the chain
+ if node.player == StateNode.OTHER:
+ node = node.parent_node
+ continue
+ chain.append(node.action)
+ node = node.parent_node
+
+ # remove the starting state, and reverse the list, so we can traverse the path
+ chain.pop()
+ chain.reverse()
+ log.info("Chosing path: %s" % (" ".join(map(str, chain))))
+ self.play_queue = chain
View
86 lib/model.py
@@ -65,44 +65,12 @@ def __eq__(self, other):
self.to_id == other.to_id:
return True
return False
- return False
def __ne__(self, 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):
NUM_STACKS = 4
@@ -149,10 +117,11 @@ def get_player(self, other=False):
" Return a players cards "
if not other:
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):
" Return a copy of the current view as a GameState object for the player. "
+ return GameState(self)
@classmethod
@@ -247,3 +216,54 @@ def is_won(self):
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
+
+
View
2 lib/player.py
@@ -13,7 +13,7 @@
class Player(object):
" 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.
The player is given the two maps and a list of center stacks. The first map,
View
4 lib/snm.py
@@ -7,7 +7,6 @@
from cardmodels import Suits, Card
import logging
import logging.config
-import time
from player import HumanPlayer
from agent import ComputerPlayer
@@ -19,7 +18,7 @@ class SpiteAndMalice(object):
def __init__(self):
self.model = SpiteAndMaliceModel()
- self.players = [HumanPlayer(), HumanPlayer()]
+ self.players = [HumanPlayer(), ComputerPlayer()]
self.view = GameView(self.model, self.players)
def run(self):
@@ -57,6 +56,7 @@ def run(self):
player_move = self.players[active_player].play_card(game_state)
if player_move == None:
+ log.warn('Got None move. Ending')
return
# play the move
View
2 snm
@@ -1,4 +1,4 @@
#!/bin/bash
-export PYTHONPATH=~/media/code/dnlib:./lib/DeckOfCards
+export PYTHONPATH=~/media/code/dnlib
python ./lib/snm.py

0 comments on commit 5931471

Please sign in to comment.
Something went wrong with that request. Please try again.