#!/usr/bin/env python # -*- mode: python; coding: utf-8; -*- # ---------------------------------------------------------------------------## # # Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer # Copyright (C) 2003 Mt. Hood Playing Card Co. # Copyright (C) 2005-2009 Skomoroh # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # ---------------------------------------------------------------------------## import os import re import subprocess import time from io import BytesIO from pysollib.mfxutil import destruct from pysollib.pysolrandom import construct_random from pysollib.settings import DEBUG, FCS_COMMAND from pysollib.util import KING FCS_VERSION = None # ************************************************************************ # * HintInterface is an abstract class that defines the public # * interface - it only consists of the constructor # * and the getHints() method. # * # * The whole hint system is exclusively used by Game.getHints(). # ************************************************************************ class HintInterface: # level == 0: show hint (key `H') # level == 1: show hint and display score value (key `Ctrl-H') # level == 2: demo def __init__(self, game, level): pass # Compute all hints for the current position. # Subclass responsibility. # # Returns a list of "atomic hints" - an atomic hint is a 7-tuple # (score, pos, ncards, from_stack, to_stack, text_color, forced_move). # # if ncards == 0: deal cards # elif from_stack == to_stack: flip card # else: move cards from from_stack to to_stack # # score, pos and text_color are only for debugging. # A forced_move is the next move that must be taken after this move # in order to avoid endless loops during demo play. # # Deal and flip may only happen if self.level >= 2 (i.e. demo). # # See Game.showHint() for more information. def getHints(self, taken_hint=None): return [] # ************************************************************************ # * AbstractHint provides a useful framework for derived hint classes. # * # * Subclasses should override computeHints() # ************************************************************************ class AbstractHint(HintInterface): def __init__(self, game, level): self.game = game self.level = level self.score_flatten_value = 0 if self.level == 0: self.score_flatten_value = 10000 # temporaries within getHints() self.bonus_color = None # self.__clones = [] self.reset() def __del__(self): self.reset() def reset(self): self.hints = [] self.max_score = 0 self.__destructClones() self.solver_state = 'not_started' # # stack cloning # # Create a shallow copy of a stack. class AClonedStack: def __init__(self, stack, stackcards): # copy class identity self.__class__ = stack.__class__ # copy model data (reference copy) stack.copyModel(self) # set new cards (shallow copy of the card list) self.cards = stackcards[:] def ClonedStack(self, stack, stackcards): s = self.AClonedStack(stack, stackcards) self.__clones.append(s) return s def __destructClones(self): for s in self.__clones: s.__class__ = self.AClonedStack # restore orignal class destruct(s) self.__clones = [] # When computing hints for level 0, the scores are flattened # (rounded down) to a multiple of score_flatten_value. # # The idea is that hints will appear equal within a certain score range # so that the player will not get confused by the demo-intelligence. # # Pressing `Ctrl-H' (level 1) will preserve the score. def addHint(self, score, ncards, from_stack, to_stack, text_color=None, forced_move=None): if score < 0: return self.max_score = max(self.max_score, score) # add an atomic hint if self.score_flatten_value > 0: score = (score // self.score_flatten_value) * \ self.score_flatten_value if text_color is None: text_color = self.BLACK assert forced_move is None or len(forced_move) == 7 # pos is used for preserving the original sort order on equal scores pos = -len(self.hints) ah = (int(score), pos, ncards, from_stack, to_stack, text_color, forced_move) self.hints.append(ah) # clean up and return hints sorted by score def _returnHints(self): hints = self.hints self.reset() hints.sort() hints.reverse() return hints # # getHints() default implementation: # - handle forced moves # - try to flip face-down cards # - call computeHints() to do something useful # - try to deal cards # - clean up and return hints sorted by score # # Default scores for flip and deal moves. SCORE_FLIP = 100000 # 0..100000 SCORE_DEAL = 0 # 0..100000 def getHints(self, taken_hint=None): # 0) setup self.reset() game = self.game # 1) forced moves of the prev. taken hint have absolute priority if taken_hint and taken_hint[6]: return [taken_hint[6]] # 2) try if we can flip a card if self.level >= 2: for r in game.allstacks: if r.canFlipCard(): self.addHint(self.SCORE_FLIP, 1, r, r) if self.SCORE_FLIP >= 90000: return self._returnHints() # 3) ask subclass to do something useful self.computeHints() # 4) try if we can deal cards if self.level >= 2: if game.canDealCards(): self.addHint(self.SCORE_DEAL, 0, game.s.talon, None) # A few games have multiple waste stacks. In these games, # reserves are used for the waste stacks. This logic will # handle for those games. if (not game.canDealCards() and game.s.waste is not None and len(game.s.waste.cards) > 0 and len(game.s.reserves) > 0): max_cards = 0 reserve = None for r in game.s.reserves: if r.acceptsCards(game.s.waste, game.s.waste.cards): if len(r.cards) < max_cards or reserve is None: max_cards = len(r.cards) reserve = r if reserve is not None: self.addHint(self.SCORE_DEAL, 1, game.s.waste, reserve) return self._returnHints() # subclass def computeHints(self): pass # # utility shallMovePile() # # we move the pile if it is accepted by the target stack def _defaultShallMovePile(self, from_stack, to_stack, pile, rpile): if from_stack is to_stack or not \ to_stack.acceptsCards(from_stack, pile): return 0 return 1 # same, but check for loops def _cautiousShallMovePile(self, from_stack, to_stack, pile, rpile): if from_stack is to_stack or not \ to_stack.acceptsCards(from_stack, pile): return 0 # if len(rpile) == 0: return 1 # now check for loops rr = self.ClonedStack(from_stack, stackcards=rpile) if rr.acceptsCards(to_stack, pile): # the pile we are going to move could be moved back - # this is dangerous as we can create endless loops... return 0 return 1 # same, but only check for loops only when in demo mode def _cautiousDemoShallMovePile(self, from_stack, to_stack, pile, rpile): if from_stack is to_stack or not \ to_stack.acceptsCards(from_stack, pile): return 0 if self.level >= 2: # if len(rpile) == 0: return 1 # now check for loops rr = self.ClonedStack(from_stack, stackcards=rpile) if rr.acceptsCards(to_stack, pile): # the pile we are going to move could be moved back - # this is dangerous as we can create endless loops... return 0 return 1 shallMovePile = _defaultShallMovePile # # other utility methods # def _canDropAllCards(self, from_stack, stacks, stackcards): assert from_stack not in stacks return 0 # FIXME: this does not account for cards which are dropped herein # cards = pile[:] # cards.reverse() # for card in cards: # for s in stacks: # if s is not from_stack: # if s.acceptsCards(from_stack, [card]): # break # else: # return 0 # return 1 # # misc. constants # # score value so that the scores look nicer K = KING + 1 # text_color that will display the score (for debug with level 1) BLACK = "black" RED = "red" BLUE = "blue" # ************************************************************************ # * # ************************************************************************ class DefaultHint(AbstractHint): # The DefaultHint is optimized for Klondike type games # and also deals quite ok with other simple variants. # # But it completely lacks any specific strategy about game # types like Forty Thieves, FreeCell, Golf, Spider, ... # # BTW, we do not cheat ! # # bonus scoring used in _getXxxScore() below - subclass overrideable # def _preferHighRankMoves(self): return 0 # Basic bonus for moving a card. # Bonus must be in range 0..999 BONUS_DROP_CARD = 300 # 0..400 BONUS_SAME_SUIT_MOVE = 200 # 0..400 BONUS_NORMAL_MOVE = 100 # 0..400 def _getMoveCardBonus(self, r, t, pile, rpile): assert pile bonus = 0 if rpile: rr = self.ClonedStack(r, stackcards=rpile) if (rr.canDropCards(self.game.s.foundations))[0]: # the card below the pile can be dropped bonus = self.BONUS_DROP_CARD if t.cards and t.cards[-1].suit == pile[0].suit: # simple heuristics - prefer moving high-rank cards bonus += self.BONUS_SAME_SUIT_MOVE + (1 + pile[0].rank) elif self._preferHighRankMoves(): # simple heuristics - prefer moving high-rank cards bonus += self.BONUS_NORMAL_MOVE + (1 + pile[0].rank) elif rpile: # simple heuristics - prefer low-rank cards in rpile bonus += self.BONUS_NORMAL_MOVE + (self.K - rpile[-1].rank) else: # simple heuristics - prefer moving high-rank cards bonus += self.BONUS_NORMAL_MOVE + (1 + pile[0].rank) return bonus # Special bonus for facing up a card after the current move. # Bonus must be in range 0..9000 BONUS_FLIP_CARD = 1500 # 0..9000 def _getFlipSpecialBonus(self, r, t, pile, rpile): assert pile and rpile # The card below the pile can be flipped # (do not cheat and look at it !) # default: prefer a short rpile bonus = max(self.BONUS_FLIP_CARD - len(rpile), 0) return bonus # Special bonus for moving a pile from stack r to stack t. # Bonus must be in range 0..9000 BONUS_CREATE_EMPTY_ROW = 9000 # 0..9000 BONUS_CAN_DROP_ALL_CARDS = 4000 # 0..4000 BONUS_CAN_CREATE_EMPTY_ROW = 2000 # 0..4000 def _getMoveSpecialBonus(self, r, t, pile, rpile): # check if we will create an empty row if not rpile: return self.BONUS_CREATE_EMPTY_ROW # check if the card below the pile can be flipped if not rpile[-1].face_up: return self._getFlipSpecialBonus(r, t, pile, rpile) # check if all the cards below our pile could be dropped if self._canDropAllCards(r, self.game.s.foundations, stackcards=rpile): # we can drop the whole remaining pile # (and will create an empty row in the next move) # print "BONUS_CAN_DROP_ALL_CARDS", r, pile, rpile self.bonus_color = self.RED return self.BONUS_CAN_DROP_ALL_CARDS + \ self.BONUS_CAN_CREATE_EMPTY_ROW # check if the cards below our pile are a whole row if r.canMoveCards(rpile): # could we move the remaining pile ? for x in self.game.s.rows: # note: we allow x == r here, because the pile # (currently at the top of r) will be # available in the next move if x is t or not x.cards: continue if x.acceptsCards(r, rpile): # we can create an empty row in the next move # print "BONUS_CAN_CREATE_EMPTY_ROW", r, x, pile, rpile self.bonus_color = self.BLUE return self.BONUS_CAN_CREATE_EMPTY_ROW return 0 # # scoring used in getHints() - subclass overrideable # # Score for moving a pile from stack r to stack t. # Increased score should be in range 0..9999 def _getMovePileScore(self, score, color, r, t, pile, rpile): assert pile self.bonus_color = color b1 = self._getMoveSpecialBonus(r, t, pile, rpile) assert 0 <= b1 <= 9000 b2 = self._getMoveCardBonus(r, t, pile, rpile) assert 0 <= b2 <= 999 return score + b1 + b2, self.bonus_color # Score for moving a pile (usually a single card) from the WasteStack. def _getMoveWasteScore(self, score, color, r, t, pile, rpile): assert pile self.bonus_color = color score = 30000 if t.cards: score = 31000 b2 = self._getMoveCardBonus(r, t, pile, rpile) assert 0 <= b2 <= 999 return score + b2, self.bonus_color # Score for dropping ncards from stack r to stack t. def _getDropCardScore(self, score, color, r, t, ncards): assert t is not r if ncards > 1: # drop immediately (Spider) return 93000, color pile = r.cards c = pile[-1] # compute distance to t.cap.base_rank - compare Stack.getRankDir() if t.cap.base_rank < 0: d = len(t.cards) else: d = (c.rank - t.cap.base_rank) % t.cap.mod if d > t.cap.mod // 2: d -= t.cap.mod if abs(d) <= 1: # drop Ace and 2 immediately score = 92000 elif r in self.game.sg.talonstacks: score = 25000 # less than _getMoveWasteScore() elif len(pile) == 1: # score = 50000 score = 91000 elif self._canDropAllCards( r, self.game.s.foundations, stackcards=pile[:-1]): score = 90000 color = self.RED else: # don't drop this card too eagerly - we may need it # for pile moving score = 50000 score += (self.K - c.rank) return score, color # # compute hints - main hint intelligence # def computeHints(self): game = self.game # 1) check Tableau piles self.step010(game.sg.dropstacks, game.s.rows) # 2) try if we can move part of a pile within the RowStacks # so that we can drop a card afterwards if not self.hints and self.level >= 1: self.step020(game.s.rows, game.s.foundations) # 3) try if we should move a card from a Foundation to a RowStack if not self.hints and self.level >= 1: self.step030(game.s.foundations, game.s.rows, game.sg.dropstacks) # 4) try if we can move a card from a RowStack to a ReserveStack if not self.hints or self.level == 0: self.step040(game.s.rows, game.sg.reservestacks) # 5) try if we should move a card from a ReserveStack to a RowStack if not self.hints or self.level == 0: self.step050(game.sg.reservestacks, game.s.rows) # Don't be too clever and give up ;-) # # implementation of the hint steps # # 1) check Tableau piles def step010(self, dropstacks, rows): # for each stack for r in dropstacks: # 1a) try if we can drop cards t, ncards = r.canDropCards(self.game.s.foundations) if t: score, color = 0, None score, color = self._getDropCardScore( score, color, r, t, ncards) self.addHint(score, ncards, r, t, color) if score >= 90000 and self.level >= 1: break # 1b) try if we can move cards to one of the RowStacks for pile in self.step010b_getPiles(r): if pile: self.step010_movePile(r, pile, rows) def step010b_getPiles(self, stack): # return all moveable piles for this stack, longest one first return (stack.getPile(), ) def step010_movePile(self, r, pile, rows): lp = len(pile) lr = len(r.cards) assert 1 <= lp <= lr rpile = r.cards[: (lr-lp)] # remaining pile empty_row_seen = 0 r_is_waste = r in self.game.sg.talonstacks for t in rows: score, color = 0, None if not self.shallMovePile(r, t, pile, rpile): continue if r_is_waste: # moving a card from the WasteStack score, color = self._getMoveWasteScore( score, color, r, t, pile, rpile) else: if not t.cards: # the target stack is empty if lp == lr: # do not move a whole stack from row to row continue if empty_row_seen: # only make one hint for moving to an empty stack # (in case we have multiple empty stacks) continue score = 60000 empty_row_seen = 1 else: # the target stack is not empty score = 80000 score, color = self._getMovePileScore( score, color, r, t, pile, rpile) self.addHint(score, lp, r, t, color) # 2) try if we can move part of a pile within the RowStacks # so that we can drop a card afterwards # score: 40000 .. 59999 step020_getPiles = step010b_getPiles def step020(self, rows, foundations): for r in rows: for pile in self.step020_getPiles(r): if not pile or len(pile) < 2: continue # is there a card in our pile that could be dropped ? drop_info = [] i = 0 for c in pile: rr = self.ClonedStack(r, stackcards=[c]) stack, ncards = rr.canDropCards(foundations) if stack and stack is not r: assert ncards == 1 drop_info.append((c, stack, ncards, i)) i += 1 # now try to make a move so that the drop-card will get free for di in drop_info: c = di[0] sub_pile = pile[di[3]+1:] # print "trying drop move", c, pile, sub_pile # assert r.canMoveCards(sub_pile) if not r.canMoveCards(sub_pile): continue for t in rows: if t is r or not t.acceptsCards(r, sub_pile): continue # print "drop move", r, t, sub_pile score = 40000 score += 1000 + (self.K - r.getCard().rank) # force the drop (to avoid loops) force = (999999, 0, di[2], r, di[1], self.BLUE, None) self.addHint( score, len(sub_pile), r, t, self.RED, forced_move=force) # 3) try if we should move a card from a Foundation to a RowStack # score: 20000 .. 29999 def step030(self, foundations, rows, dropstacks): for s in foundations: card = s.getCard() if not card or not s.canMoveCards([card]): continue # search a RowStack that would accept the card for t in rows: if t is s or not t.acceptsCards(s, [card]): continue tt = self.ClonedStack(t, stackcards=t.cards+[card]) # search a Stack that would benefit from this card for r in dropstacks: if r is t: continue pile = r.getPile() if not pile: continue if not tt.acceptsCards(r, pile): continue # compute remaining pile in r rpile = r.cards[:(len(r.cards)-len(pile))] rr = self.ClonedStack(r, stackcards=rpile) if rr.acceptsCards(t, pile): # the pile we are going to move from r to t # could be moved back from t ro r - this is # dangerous as we can create loops... continue score = 20000 + card.rank # print score, s, t, r, pile, rpile # force the move from r to t (to avoid loops) force = (999999, 0, len(pile), r, t, self.BLUE, None) self.addHint(score, 1, s, t, self.BLUE, forced_move=force) # 4) try if we can move a card from a RowStack to a ReserveStack # score: 10000 .. 19999 def step040(self, rows, reservestacks): if not reservestacks: return for r in rows: card = r.getCard() if not card or not r.canMoveCards([card]): continue pile = [card] # compute remaining pile in r rpile = r.cards[:(len(r.cards)-len(pile))] rr = self.ClonedStack(r, stackcards=rpile) for t in reservestacks: if t is r or not t.acceptsCards(r, pile): continue if rr.acceptsCards(t, pile): # the pile we are going to move from r to t # could be moved back from t ro r - this is # dangerous as we can create loops... continue score = 10000 score, color = self._getMovePileScore( score, None, r, t, pile, rpile) self.addHint(score, len(pile), r, t, color) break # 5) try if we should move a card from a ReserveStack to a RowStack def step050(self, reservestacks, rows): if not reservestacks: return # FIXME # ************************************************************************ # * # ************************************************************************ class CautiousDefaultHint(DefaultHint): shallMovePile = DefaultHint._cautiousShallMovePile # shallMovePile = DefaultHint._cautiousDemoShallMovePile def _preferHighRankMoves(self): return 1 # ************************************************************************ # * now some default hints for the various game types # ************************************************************************ # DefaultHint is optimized for Klondike type games anyway class KlondikeType_Hint(DefaultHint): pass # this works for Yukon, but not too well for Russian Solitaire class YukonType_Hint(CautiousDefaultHint): def step010b_getPiles(self, stack): # return all moveable piles for this stack, longest one first p = stack.getPile() piles = [] while p: piles.append(p) p = p[1:] # note: we need a fresh shallow copy return piles class Yukon_Hint(YukonType_Hint): BONUS_FLIP_CARD = 9000 BONUS_CREATE_EMPTY_ROW = 100 # FIXME: this is only a rough approximation and doesn't seem to help # for Russian Solitaire def _getMovePileScore(self, score, color, r, t, pile, rpile): s, color = YukonType_Hint._getMovePileScore( self, score, color, r, t, pile, rpile) bonus = s - score assert 0 <= bonus <= 9999 # We must take care when moving piles that we won't block cards, # i.e. if there is a card in pile which would be needed # for a card in stack t. tpile = t.getPile() if tpile: for cr in pile: rr = self.ClonedStack(r, stackcards=[cr]) for ct in tpile: if rr.acceptsCards(t, [ct]): d = bonus // 1000 bonus = (d * 1000) + bonus % 100 break return score + bonus, color class FreeCellType_Hint(CautiousDefaultHint): pass class GolfType_Hint(DefaultHint): pass class SpiderType_Hint(DefaultHint): pass class PySolHintLayoutImportError(Exception): def __init__(self, msg, cards, line_num): """docstring for __init__""" self.msg = msg self.cards = cards self.line_num = line_num def format(self): return self.msg + ":\n\n" + ', '.join(self.cards) class Base_Solver_Hint: def __init__(self, game, dialog, **game_type): self.game = game self.dialog = dialog self.game_type = game_type self.options = { 'iters_step': 100, 'max_iters': 10000, 'progress': False, 'preset': None, } self.hints = [] self.hints_index = 0 # correct cards rank if foundations.base_rank != 0 (Penguin, Opus) if 'base_rank' in game_type: # (Simple Simon) self.base_rank = game_type['base_rank'] else: self.base_rank = game.s.foundations[0].cap.base_rank def _setText(self, **kw): return self.dialog.setText(**kw) def config(self, **kw): self.options.update(kw) def _card2str_format(self, fmt, rank, suit): # row and reserves rank = (rank-self.base_rank) % 13 return fmt.format(R="A23456789TJQK"[rank], S="CSHD"[suit]) def card2str1_(self, rank, suit): # row and reserves return self._card2str_format('{R}{S}', rank, suit) def card2str1(self, card): return self.card2str1_(card.rank, card.suit) def card2str2(self, card): # foundations return self._card2str_format('{S}-{R}', card.rank, card.suit) # hard solvable: Freecell #47038300998351211829 (65539 iters) def getHints(self, taken_hint=None): if taken_hint and taken_hint[6]: return [taken_hint[6]] h = self.hints[self.hints_index] if h is None: return None ncards, src, dest = h thint = None if len(src.cards) > ncards and not src.cards[-ncards-1].face_up: # flip card thint = (999999, 0, 1, src, src, None, None) skip = False if dest is None: # foundation if src is self.game.s.talon: if not src.cards[-1].face_up: self.game.flipMove(src) dest = self.game.s.foundations[0] else: cards = src.cards[-ncards:] for f in self.game.s.foundations: if f.acceptsCards(src, cards): dest = f break assert dest self.hints_index += 1 if skip: return [] hint = (999999, 0, ncards, src, dest, None, thint) return [hint] def colonPrefixMatch(self, prefix, s): m = re.match(prefix + ': ([0-9]+)', s) if m: self._v = int(m.group(1)) return True else: self._v = None return False def run_solver(self, command, board): if DEBUG: print(command) kw = {'shell': True, 'stdin': subprocess.PIPE, 'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE} if os.name != 'nt': kw['close_fds'] = True p = subprocess.Popen(command, **kw) bytes_board = bytes(board, 'utf-8') pout, perr = p.communicate(bytes_board) if p.returncode in (127, 1): # Linux and Windows return codes for "command not found" error raise RuntimeError('Solver exited with {}'.format(p.returncode)) return BytesIO(pout), BytesIO(perr) def importFile(solver, fh, s_game, self): s_game.endGame() s_game.random = construct_random('Custom') s_game.newGame( shuffle=True, random=construct_random('Custom'), dealer=lambda: solver.importFileHelper(fh, s_game)) s_game.random = construct_random('Custom') def importFileHelper(solver, fh, s_game): pass use_fc_solve_lib = False try: import freecell_solver fc_solve_lib_obj = freecell_solver.FreecellSolver() use_fc_solve_lib = True except BaseException: pass use_bh_solve_lib = False try: import black_hole_solver bh_solve_lib_obj = black_hole_solver.BlackHoleSolver() use_bh_solve_lib = True except BaseException: pass class FreeCellSolver_Hint(Base_Solver_Hint): def _determineIfSolverState(self, line): if re.search('^(?:Iterations count exceeded)', line): self.solver_state = 'intractable' return True elif re.search('^(?:I could not solve this game)', line): self.solver_state = 'unsolved' return True else: return False def _isSimpleSimon(self): game_type = self.game_type return ('preset' in game_type and game_type['preset'] == 'simple_simon') def _addBoardLine(self, line): self.board += line + '\n' return def _addPrefixLine(self, prefix, b): if b: self._addBoardLine(prefix + b) return def importFileHelper(solver, fh, s_game): game = s_game.s stack_idx = 0 RANKS_S = "A23456789TJQK" RANKS0_S = '0' + RANKS_S RANKS_RE = '(?:' + '[' + RANKS_S + ']' + '|10)' SUITS_S = "CSHD" SUITS_RE = '[' + SUITS_S + ']' CARD_RE = r'(?:' + RANKS_RE + SUITS_RE + ')' line_num = 0 def cards(): return game.talon.cards def put(target, suit, rank): ret = [i for i, c in enumerate(cards()) if c.suit == suit and c.rank == rank] if len(ret) < 1: raise PySolHintLayoutImportError( "Duplicate cards in input", [solver.card2str1_(rank, suit)], line_num ) ret = ret[0] game.talon.cards = \ cards()[0:ret] + cards()[(ret+1):] + [cards()[ret]] s_game.flipMove(game.talon) s_game.moveMove(1, game.talon, target, frames=0) def put_str(target, str_): put(target, SUITS_S.index(str_[-1]), (RANKS_S.index(str_[0]) if len(str_) == 2 else 9)) def my_find_re(RE, m, msg): s = m.group(1) if not re.match(r'^\s*(?:' + RE + r')?(?:\s+' + RE + r')*\s*$', s): raise PySolHintLayoutImportError( msg, [], line_num ) return re.findall(r'\b' + RE + r'\b', s) # Based on https://stackoverflow.com/questions/8898294 - thanks! def mydecode(s): for encoding in "utf-8-sig", "utf-8": try: return s.decode(encoding) except UnicodeDecodeError: continue return s.decode("latin-1") # will always work mytext = mydecode(fh.read()) for line_p in mytext.splitlines(): line_num += 1 line = line_p.rstrip('\r\n') m = re.match(r'^(?:Foundations:|Founds?:)\s*(.*)', line) if m: for gm in my_find_re( r'(' + SUITS_RE + r')-([' + RANKS0_S + r'])', m, "Invalid Foundations line"): for foundat in game.foundations: suit = foundat.cap.suit if SUITS_S[suit] == gm[0]: rank = gm[1] if len(rank) == 1: lim = RANKS0_S.index(rank) else: lim = 10 for r in range(lim): put(foundat, suit, r) break continue m = re.match(r'^(?:FC:|Freecells:)\s*(.*)', line) if m: g = my_find_re(r'(' + CARD_RE + r'|\-)', m, "Invalid Freecells line") while len(g) < len(game.reserves): g.append('-') for i, gm in enumerate(g): str_ = gm if str_ != '-': put_str(game.reserves[i], str_) continue m = re.match(r'^:?\s*(.*)', line) for str_ in my_find_re(r'(' + CARD_RE + r')', m, "Invalid column text"): put_str(game.rows[stack_idx], str_) stack_idx += 1 if len(cards()) > 0: raise PySolHintLayoutImportError( "Missing cards in input", [solver.card2str1(c) for c in cards()], -1 ) def calcBoardString(self): game = self.game self.board = '' is_simple_simon = self._isSimpleSimon() b = '' for s in game.s.foundations: if s.cards: b += ' ' + self.card2str2( s.cards[0 if is_simple_simon else -1]) self._addPrefixLine('Founds:', b) b = '' for s in game.s.reserves: b += ' ' + (self.card2str1(s.cards[-1]) if s.cards else '-') self._addPrefixLine('FC:', b) for s in game.s.rows: b = '' for c in s.cards: cs = self.card2str1(c) if not c.face_up: cs = '<%s>' % cs b += cs + ' ' self._addBoardLine(b.strip()) return self.board def computeHints(self): game = self.game game_type = self.game_type global FCS_VERSION if FCS_VERSION is None: if use_fc_solve_lib: FCS_VERSION = (5, 0, 0) else: pout, _ = self.run_solver(FCS_COMMAND + ' --version', '') s = str(pout.read(), encoding='utf-8') m = re.search(r'version ([0-9]+)\.([0-9]+)\.([0-9]+)', s) if m: FCS_VERSION = (int(m.group(1)), int(m.group(2)), int(m.group(3))) else: FCS_VERSION = (0, 0, 0) progress = self.options['progress'] board = self.calcBoardString() if DEBUG: print('--------------------\n', board, '--------------------') args = [] if use_fc_solve_lib: args += ['--reset', '-opt', ] else: args += ['-m', '-p', '-opt', '-sel'] if FCS_VERSION >= (4, 20, 0): args += ['-hoi'] if (not use_fc_solve_lib) and progress: args += ['--iter-output'] fcs_iter_output_step = None if FCS_VERSION >= (4, 20, 0): fcs_iter_output_step = self.options['iters_step'] args += ['--iter-output-step', str(fcs_iter_output_step)] if DEBUG: args += ['-s'] if self.options['preset'] and self.options['preset'] != 'none': args += ['--load-config', self.options['preset']] args += ['--max-iters', str(self.options['max_iters']), '--decks-num', str(game.gameinfo.decks), '--stacks-num', str(len(game.s.rows)), '--freecells-num', str(len(game.s.reserves)), ] if 'preset' in game_type: args += ['--preset', game_type['preset']] if 'sbb' in game_type: args += ['--sequences-are-built-by', game_type['sbb']] if 'sm' in game_type: args += ['--sequence-move', game_type['sm']] if 'esf' in game_type: args += ['--empty-stacks-filled-by', game_type['esf']] if use_fc_solve_lib: fc_solve_lib_obj.input_cmd_line(args) status = fc_solve_lib_obj.solve_board(board) else: command = FCS_COMMAND+' '+' '.join(args) pout, perr = self.run_solver(command, board) self.solver_state = 'unknown' stack_types = { 'the': game.s.foundations, 'stack': game.s.rows, 'freecell': game.s.reserves, } if DEBUG: start_time = time.time() if not (use_fc_solve_lib) and progress: iter_ = 0 depth = 0 states = 0 for sbytes in pout: s = str(sbytes, encoding='utf-8') if DEBUG >= 5: print(s) if self.colonPrefixMatch('Iteration', s): iter_ = self._v elif self.colonPrefixMatch('Depth', s): depth = self._v elif self.colonPrefixMatch('Stored-States', s): states = self._v if iter_ % 100 == 0 or fcs_iter_output_step: self._setText(iter=iter_, depth=depth, states=states) elif re.search('^(?:-=-=)', s): break elif self._determineIfSolverState(s): break self._setText(iter=iter_, depth=depth, states=states) hints = [] if use_fc_solve_lib: self._setText( iter=fc_solve_lib_obj.get_num_times(), depth=0, states=fc_solve_lib_obj.get_num_states_in_collection(), ) if status == 0: m = fc_solve_lib_obj.get_next_move() while m: type_ = ord(m.s[0]) src = ord(m.s[1]) dest = ord(m.s[2]) hints.append([ (ord(m.s[3]) if type_ == 0 else (13 if type_ == 11 else 1)), (game.s.rows if (type_ in [0, 1, 4, 11, ]) else game.s.reserves)[src], (game.s.rows[dest] if (type_ in [0, 2]) else (game.s.reserves[dest] if (type_ in [1, 3]) else None))]) m = fc_solve_lib_obj.get_next_move() else: self.solver_state = 'unsolved' else: for sbytes in pout: s = str(sbytes, encoding='utf-8') if DEBUG: print(s) if self._determineIfSolverState(s): break m = re.match( 'Total number of states checked is ([0-9]+)\\.', s) if m: self._setText(iter=int(m.group(1))) m = re.match('This scan generated ([0-9]+) states\\.', s) if m: self._setText(states=int(m.group(1))) m = re.match('Move (.*)', s) if not m: continue move_s = m.group(1) m = re.match( 'the sequence on top of Stack ([0-9]+) to the foundations', move_s) if m: ncards = 13 st = stack_types['stack'] sn = int(m.group(1)) src = st[sn] dest = None else: m = re.match( '(?Pa card|(?P[0-9]+) cards) ' 'from (?Pstack|freecell) ' '(?P[0-9]+) to ' '(?Pthe foundations|' '(?Pfreecell|stack) ' '(?P[0-9]+))\\s*', move_s) if not m: continue if m.group('ncards') == 'a card': ncards = 1 else: ncards = int(m.group('count')) st = stack_types[m.group('source_type')] sn = int(m.group('source_idx')) src = st[sn] dest_s = m.group('dest') if dest_s == 'the foundations': dest = None else: dt = stack_types[m.group('dest_type')] dest = dt[int(m.group('dest_idx'))] hints.append([ncards, src, dest]) if DEBUG: print('time:', time.time()-start_time) self.hints = hints if len(hints) > 0: if self.solver_state != 'intractable': self.solver_state = 'solved' self.hints.append(None) if not use_fc_solve_lib: pout.close() perr.close() class BlackHoleSolver_Hint(Base_Solver_Hint): BLACK_HOLE_SOLVER_COMMAND = 'black-hole-solve' def importFileHelper(solver, fh, s_game): game = s_game.s stack_idx = 0 found_idx = 0 RANKS_S = "A23456789TJQK" RANKS_RE = '(?:' + '[' + RANKS_S + ']' + '|10)' SUITS_S = "CSHD" SUITS_RE = '[' + SUITS_S + ']' CARD_RE = r'(?:' + RANKS_RE + SUITS_RE + ')' line_num = 0 def cards(): return game.talon.cards def put(target, suit, rank): ret = [i for i, c in enumerate(cards()) if c.suit == suit and c.rank == rank] if len(ret) < 1: raise PySolHintLayoutImportError( "Duplicate cards in input", [solver.card2str1_(rank, suit)], line_num ) ret = ret[0] game.talon.cards = \ cards()[0:ret] + cards()[(ret+1):] + [cards()[ret]] s_game.flipMove(game.talon) s_game.moveMove(1, game.talon, target, frames=0) def put_str(target, str_): put(target, SUITS_S.index(str_[-1]), (RANKS_S.index(str_[0]) if len(str_) == 2 else 9)) def my_find_re(RE, m, msg): s = m.group(1) if not re.match(r'^\s*(?:' + RE + r')?(?:\s+' + RE + r')*\s*$', s): raise PySolHintLayoutImportError( msg, [], line_num ) return re.findall(r'\b' + RE + r'\b', s) # Based on https://stackoverflow.com/questions/8898294 - thanks! def mydecode(s): for encoding in "utf-8-sig", "utf-8": try: return s.decode(encoding) except UnicodeDecodeError: continue return s.decode("latin-1") # will always work mytext = mydecode(fh.read()) for line_p in mytext.splitlines(): line_num += 1 line = line_p.rstrip('\r\n') m = re.match(r'^(?:Foundations:|Founds?:)\s*(.*)', line) if m: for gm in my_find_re(r'(' + CARD_RE + r')', m, "Invalid Foundations line"): put_str(game.foundations[found_idx], gm) found_idx += 1 continue m = re.match(r'^:?\s*(.*)', line) for str_ in my_find_re(r'(' + CARD_RE + r')', m, "Invalid column text"): put_str(game.rows[stack_idx], str_) stack_idx += 1 if len(cards()) > 0: # A bit hacky, but normally, this move would require an internal. # We don't want to have to add an internal stack to all Black # Hole Solver games just for the import. s_game.moveMove(1, game.foundations[0], game.rows[0], frames=0) s_game.moveMove(len(cards()), game.talon, game.foundations[0], frames=0) s_game.moveMove(1, game.rows[0], game.foundations[0], frames=0) def calcBoardString(self): board = '' cards = self.game.s.talon.cards if len(cards) > 0: board += ' '.join(['Talon:'] + [self.card2str1(x) for x in reversed(cards)]) board += '\n' board += 'Foundations:' for f in self.game.s.foundations: cards = f.cards s = '-' if len(cards) > 0: s = self.card2str1(cards[-1]) board += ' ' + s board += '\n' for s in self.game.s.rows: b = '' for c in s.cards: cs = self.card2str1(c) if not c.face_up: cs = '<%s>' % cs b += cs + ' ' board += b.strip() + '\n' return board def computeHints(self): game = self.game game_type = self.game_type board = self.calcBoardString() if DEBUG: print('--------------------\n', board, '--------------------') if use_bh_solve_lib: # global bh_solve_lib_obj # bh_solve_lib_obj = bh_solve_lib_obj.new_bhs_user_handle() bh_solve_lib_obj.recycle() bh_solve_lib_obj.read_board( board=board, game_type=game_type['preset'], place_queens_on_kings=( game_type['queens_on_kings'] if ('queens_on_kings' in game_type) else True), wrap_ranks=( game_type['wrap_ranks'] if ('wrap_ranks' in game_type) else True), ) bh_solve_lib_obj.limit_iterations(self.options['max_iters']) else: args = [] args += ['--game', game_type['preset'], '--rank-reach-prune'] args += ['--max-iters', str(self.options['max_iters'])] if 'queens_on_kings' in game_type: args += ['--queens-on-kings'] if 'wrap_ranks' in game_type: args += ['--wrap-ranks'] command = self.BLACK_HOLE_SOLVER_COMMAND + ' ' + ' '.join(args) if DEBUG: start_time = time.time() result = '' if use_bh_solve_lib: ret_code = bh_solve_lib_obj.resume_solution() else: pout, perr = self.run_solver(command, board) for sbytes in pout: s = str(sbytes, encoding='utf-8') if DEBUG >= 5: print(s) m = re.search('^(Intractable|Unsolved|Solved)!', s.rstrip()) if m: result = m.group(1) break self._setText(iter=0, depth=0, states=0) hints = [] if use_bh_solve_lib: self.solver_state = ( 'solved' if ret_code == 0 else ('intractable' if bh_solve_lib_obj.ret_code_is_suspend(ret_code) else 'unsolved')) self._setText(iter=bh_solve_lib_obj.get_num_times()) self._setText( states=bh_solve_lib_obj.get_num_states_in_collection()) if self.solver_state == 'solved': m = bh_solve_lib_obj.get_next_move() while m: found_stack_idx = m.get_column_idx() if len(game.s.rows) > found_stack_idx >= 0: src = game.s.rows[found_stack_idx] hints.append([1, src, None]) else: hints.append([1, game.s.talon, None]) m = bh_solve_lib_obj.get_next_move() else: self.solver_state = result.lower() for sbytes in pout: s = str(sbytes, encoding='utf-8') if DEBUG: print(s) if s.strip() == 'Deal talon': hints.append([1, game.s.talon, None]) continue m = re.match( 'Total number of states checked is ([0-9]+)\\.', s) if m: self._setText(iter=int(m.group(1))) continue m = re.match('This scan generated ([0-9]+) states\\.', s) if m: self._setText(states=int(m.group(1))) continue m = re.match( 'Move a card from stack ([0-9]+) to the foundations', s) if not m: continue found_stack_idx = int(m.group(1)) src = game.s.rows[found_stack_idx] hints.append([1, src, None]) pout.close() perr.close() if DEBUG: print('time:', time.time()-start_time) hints.append(None) self.hints = hints class FreeCellSolverWrapper: def __init__(self, **game_type): self.game_type = game_type def __call__(self, game, dialog): hint = FreeCellSolver_Hint(game, dialog, **self.game_type) return hint class BlackHoleSolverWrapper: def __init__(self, **game_type): self.game_type = game_type def __call__(self, game, dialog): hint = BlackHoleSolver_Hint(game, dialog, **self.game_type) return hint