Fix some uno logic bugs
This commit is contained in:
parent
1a7f6c0c59
commit
45b68e873d
@ -1,6 +1,8 @@
|
||||
{
|
||||
"unobot": "BTN-Uno",
|
||||
"unochannel": "#BTN-uno",
|
||||
"strategy": "play_by_chains",
|
||||
"enforce_wd4": true,
|
||||
"streak_max": 3,
|
||||
"enable_autojoin": true,
|
||||
"enable_trigger": false,
|
||||
@ -8,8 +10,8 @@
|
||||
"enable_randomhuman": true,
|
||||
"randomhuman_chance": 15,
|
||||
"randomhuman_sleep": 4,
|
||||
"delay_joingame": [1.2, 12.0],
|
||||
"delay_beforepickcolor": [1.0, 4.0],
|
||||
"delay_beforedraw": [1.0, 3.0],
|
||||
"delay_beforemove": [1.0, 4.0]
|
||||
"delay_joingame": [3.2, 12.0],
|
||||
"delay_beforepickcolor": [3.0, 6.0],
|
||||
"delay_beforedraw": [3.0, 6.0],
|
||||
"delay_beforemove": [3.0, 6.0]
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ import time
|
||||
import re
|
||||
from operator import itemgetter
|
||||
from threading import Thread
|
||||
from pprint import pprint
|
||||
import logging
|
||||
|
||||
|
||||
class UnoPlay(ModuleBase):
|
||||
@ -26,6 +28,9 @@ class UnoPlay(ModuleBase):
|
||||
self.has_drawn = False
|
||||
self.has_joined = False
|
||||
self.games_played = 0
|
||||
self.strategies = {"play_high_value": self.play_high_value,
|
||||
"play_by_chains": self.play_by_chains}
|
||||
assert self.config["strategy"] in self.strategies
|
||||
self.cards = []
|
||||
|
||||
def trigger(self, args, prefix, trailing):
|
||||
@ -38,11 +43,36 @@ class UnoPlay(ModuleBase):
|
||||
self.games_played = 0
|
||||
self.join_game()
|
||||
|
||||
def join_game(self):
|
||||
if not self.has_joined:
|
||||
self.sleep("joingame")
|
||||
self.has_joined = True
|
||||
self.bot.act_PRIVMSG(self.config["unochannel"], "jo")
|
||||
def decklisten(self, args, prefix, trailing):
|
||||
"""
|
||||
Listen for messages sent via NOTICe to the bot, this is usually a list of cards in our hand. Parse them.
|
||||
"""
|
||||
if trailing.startswith("["): # anti-znc buffer playback
|
||||
return
|
||||
if self.config["unobot"] not in prefix:
|
||||
return
|
||||
|
||||
if "You don't have that card" in trailing:
|
||||
self.log.error("played invalid card!")
|
||||
return
|
||||
|
||||
trailing = self.stripcolors(trailing)
|
||||
|
||||
cards = []
|
||||
|
||||
for carddata in trailing.split(" "):
|
||||
carddata = carddata.strip()
|
||||
cards.append(self.parsecard(carddata))
|
||||
cards.sort(key=lambda tup: tup[1])
|
||||
cards.reverse()
|
||||
|
||||
self.cards = cards
|
||||
|
||||
self.log.debug(cards)
|
||||
|
||||
if self.shouldgo:
|
||||
self.shouldgo = False
|
||||
self.taketurn()
|
||||
|
||||
def unoplay(self, args, prefix, trailing):
|
||||
if trailing.startswith("["): # anti-znc buffer playback
|
||||
@ -127,6 +157,86 @@ class UnoPlay(ModuleBase):
|
||||
else:
|
||||
self.join_game()
|
||||
|
||||
def play_high_value(self):
|
||||
"""
|
||||
Self.cards is sorted by card value by default. This strategy searches for a move by finding the mostly highly
|
||||
valued card that is a valid move.
|
||||
"""
|
||||
# Play anything thats not a wild
|
||||
for card in self.cards:
|
||||
# Skip wilds for now
|
||||
if card[0] in ["wd4", "w"]:
|
||||
continue
|
||||
if self.validate(card, self.current_card):
|
||||
return card
|
||||
|
||||
# Play anything
|
||||
for card in self.cards:
|
||||
if self.validate(card, self.current_card):
|
||||
return card
|
||||
|
||||
# Give up
|
||||
return None
|
||||
|
||||
def play_by_chains(self):
|
||||
"""
|
||||
Find all legal permutations starting with the card in play based on our hand. The first card of the chain with
|
||||
the highest point sum will be selected for play.
|
||||
"""
|
||||
def chain_next(cards, chain):
|
||||
"""
|
||||
Given some cards, (Cards == list of card-like data structures)
|
||||
And given a chain, (chain == list of cards where the last entry is the card we play on top of)
|
||||
Return a list of all chains formed by appending valid moves to the chain
|
||||
"""
|
||||
# Find cards we can legally append to the chain - all wilds and valid moves are accepted
|
||||
valid_nexts = [card for card in cards if chain[-1][0].startswith("w") or
|
||||
self.validate(card, chain[-1], cards)]
|
||||
|
||||
if valid_nexts:
|
||||
# If we can make a move, permutate subchains per valid move
|
||||
subchains = []
|
||||
for card in valid_nexts:
|
||||
child_chain = chain[:]
|
||||
child_chain.append(card)
|
||||
child_cards = cards[:]
|
||||
child_cards.remove(card)
|
||||
child_cards.sort(reverse=True, key=lambda x: x[1]) # Explore high value cards first (does this help?)
|
||||
assert len(child_cards) == len(cards) - 1
|
||||
for subchain in chain_next(child_cards, child_chain):
|
||||
subchains.append(subchain)
|
||||
return subchains
|
||||
else:
|
||||
# If we can't play, we found the end of a chain. Return the chain
|
||||
return [chain]
|
||||
|
||||
# Get chains with at least one card added by us
|
||||
chains = [[i, 0] for i in chain_next(self.cards, [self.current_card]) if len(i) > 1]
|
||||
for chain in chains:
|
||||
for card in chain[0][1:]:
|
||||
chain[1] += card[1]
|
||||
|
||||
chains.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
# We now have a list of sets like:
|
||||
# (chain, chain_value)
|
||||
# Where chain is a list of card structs and chain_value is the point value of that chain.
|
||||
# The list is sorted by chain_value
|
||||
# pprint(chains)
|
||||
self.log.info("Cards in hand: {}. Considering {} possible outcomes...".format(len(self.cards), len(chains)))
|
||||
|
||||
if not chains:
|
||||
return None # No valid moves :(
|
||||
|
||||
selected_chain, value = chains[0]
|
||||
return selected_chain[1]
|
||||
|
||||
def join_game(self):
|
||||
if not self.has_joined:
|
||||
self.sleep("joingame")
|
||||
self.has_joined = True
|
||||
self.bot.act_PRIVMSG(self.config["unochannel"], "jo")
|
||||
|
||||
def send_later(self, channel, msg, area):
|
||||
Thread(target=self._send_later, args=(self.bot.act_PRIVMSG, (channel, msg, ), area)).start()
|
||||
|
||||
@ -147,7 +257,7 @@ class UnoPlay(ModuleBase):
|
||||
if card[2]["color"] in mycolors.keys():
|
||||
mycolors[card[2]["color"]] += 1
|
||||
|
||||
mycolors = sorted(mycolors.items(), key=itemgetter(1))
|
||||
mycolors = sorted(mycolors.items(), key=lambda x: x[1])
|
||||
mycolors.reverse()
|
||||
|
||||
self.log.debug("Sorted: %s" % str(mycolors))
|
||||
@ -182,30 +292,34 @@ class UnoPlay(ModuleBase):
|
||||
self.playcard(move[0])
|
||||
|
||||
def getbestmove(self):
|
||||
# Play anything thats not a wild
|
||||
for card in self.cards:
|
||||
# Skip wilds for now
|
||||
if card[0] in ["wd4", "w"]:
|
||||
continue
|
||||
if self.validate(card, self.current_card):
|
||||
return card
|
||||
"""
|
||||
Depend inon the set strategy, determine the best card to play
|
||||
"""
|
||||
return self.strategies[self.config["strategy"]]()
|
||||
|
||||
# Play anything
|
||||
for card in self.cards:
|
||||
if self.validate(card, self.current_card):
|
||||
return card
|
||||
|
||||
# Give up
|
||||
return None
|
||||
|
||||
def validate(self, newcard, basecard):
|
||||
def validate(self, newcard, basecard, other_cards=None, colors_only=False):
|
||||
"""
|
||||
Determine if it is a legal move to play newcard on top of basecard
|
||||
:param newcard: the card you want to try to play
|
||||
:param basecard: the card you play on top of
|
||||
:param other_cards: if WD4 rule enforcement is on, consider these other cards in-hand when selecting a WD4
|
||||
"""
|
||||
nc = newcard[2]
|
||||
bc = basecard[2]
|
||||
|
||||
# Wilds can always be played
|
||||
if nc["type"] in ["wd4", "w"]:
|
||||
if nc["type"] in ["w"]:
|
||||
return True
|
||||
|
||||
# Wilds can always be played
|
||||
if nc["type"] in ["wd4"]:
|
||||
# WD4 can only be played if there are no non-wd4 moves playable
|
||||
if self.config["enforce_wd4"] and other_cards:
|
||||
other_valid = any([self.validate(i, basecard) for i in other_cards if i[0] != "wd4"])
|
||||
if not other_valid:
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
# Color matches can always be played
|
||||
if nc["color"] == bc["color"]:
|
||||
return True
|
||||
@ -225,32 +339,18 @@ class UnoPlay(ModuleBase):
|
||||
def playcard(self, card):
|
||||
self.bot.act_PRIVMSG(self.config["unochannel"], "pl %s" % card)
|
||||
|
||||
def decklisten(self, args, prefix, trailing):
|
||||
if trailing.startswith("["): # anti-znc buffer playback
|
||||
return
|
||||
if self.config["unobot"] not in prefix:
|
||||
return
|
||||
|
||||
trailing = self.stripcolors(trailing)
|
||||
|
||||
cards = []
|
||||
|
||||
for carddata in trailing.split(" "):
|
||||
carddata = carddata.strip()
|
||||
cards.append(self.parsecard(carddata))
|
||||
cards.sort(key=lambda tup: tup[1])
|
||||
cards.reverse()
|
||||
|
||||
self.cards = cards
|
||||
|
||||
self.log.debug(cards)
|
||||
|
||||
if self.shouldgo:
|
||||
self.shouldgo = False
|
||||
self.taketurn()
|
||||
|
||||
def parsecard(self, input):
|
||||
# returns a card, weight tuple
|
||||
try:
|
||||
return self._parsecard(input)
|
||||
except:
|
||||
logging.error("Failed to parse card: {}".format(repr(input)))
|
||||
raise
|
||||
|
||||
def _parsecard(self, input):
|
||||
"""
|
||||
Given a card PMed to our bot, parse it into a card data structure, e.g.:
|
||||
('r1',1, {'type': 'num', 'number': 1, 'color': 'r'}),
|
||||
"""
|
||||
self.log.debug("Parse %s" % input)
|
||||
# Colors
|
||||
colors = {
|
||||
@ -319,7 +419,23 @@ class UnoPlay(ModuleBase):
|
||||
return (color + card, weight, cardinfo)
|
||||
|
||||
def stripcolors(self, input):
|
||||
"""
|
||||
Strip color codes from an IRC messages
|
||||
"""
|
||||
return re.sub(r'\\x0([23])(([0-9]{1,2}((,[0-9]{1,2})?))?)', '', repr(input))[1:-1]
|
||||
|
||||
# def ondisable(self):
|
||||
# pass
|
||||
|
||||
class TestStrategy(UnoPlay):
|
||||
def __init__(self):
|
||||
self.current_card = ('y1', 1, {'type': 'num', 'color': 'y', 'number': 1})
|
||||
self.cards = [('wd4', 30, {'type': 'wd4', 'color': None, 'number': None}),
|
||||
('br', 14, {'type': 'r', 'color': 'b', 'number': None}),
|
||||
('br', 14, {'type': 'r', 'color': 'b', 'number': None}),
|
||||
('g1', 1, {'type': 'num', 'color': 'g', 'number': 1}),
|
||||
('b2', 2, {'type': 'num', 'color': 'b', 'number': 2}),
|
||||
('r1', 1, {'type': 'num', 'color': 'r', 'number': 1})]
|
||||
self.log = logging.getLogger('TestLog')
|
||||
|
||||
if __name__ == '__main__':
|
||||
t = TestStrategy()
|
||||
pprint(t.play_by_chains())
|
||||
|
Loading…
Reference in New Issue
Block a user