This is combinatorial optimization . Your algorithm creates only one solution. This is called the "greedy algorithm . " For most problems, there is no greedy algorithm that can guarantee an optimal solution. This seems to apply to your issue as well.
So, if you want to improve the results, you must create some or even all possible solutions and choose the best. Thanks to an efficient approach and proper pruning of the search tree, you can always find the best solution for your problem size.
It might be a good idea to split the solution generation into the "initial phase" (creating runs and sets with a minimum size of 3), and then the "filling phase" (where the rest of the cards are added to the existing ones and starts and sets).
With each non-assigned card (at the beginning, all cards are not assigned), you have several possible “moves”. At the initial stage, these can be the following types: skip, start a run, or run a set. In the "filling phase" the moves can be as follows: skip, add to the run, add to the set (there may be several options for adding a card to the move).
The following rules will be useful for trimming, since they will not affect the best solution value found:
- do not use a wild card in place of an unassigned card.
- do not run a new set of the same rank as the existing one.
I am not very familiar with JavaScript, so here is a solution with Python (maybe you can use this or that idea):
# trying to solve # http://stackoverflow.com/questions/31615597/combinatorics-of-cardgame-hand-evaluation-for-runs-with-wildcards-and-duplicate import copy CLUBS, DIAMONDS, HEARTS, SPADES, STARS = range(5) WILD = 0 testHand = [(CLUBS,3), WILD, (SPADES,3), (CLUBS,4), (CLUBS,5), (CLUBS,6), (CLUBS,6), (CLUBS,7), (CLUBS,8), (SPADES,10), (HEARTS,10), WILD] def nextInRun(card): if card[1] == 13: raise ValueError("cannot determine next card in run for %s" % str(card)) return (card[0],card[1]+1) def prevInRun(card): if card[1] == 3: raise ValueError("cannot determine previous card in run for %s" % str(card)) return (card[0],card[1]-1) def fit2Set(card1, card2): return card1 == WILD or card2 == WILD or card1[1] == card2[1] START_RUN, START_SET = range(2) def removeCard(hand, card, startIdx=None): hand = copy.deepcopy(hand) if startIdx != None and hand.index(card) < startIdx: startIdx -= 1 hand.remove(card) return ((hand, startIdx) if startIdx != None else hand) def startMoves(handr1,card1,startIdx): if card1 == WILD: #print "trying to start run with 1 WILD card" for card2 in set(handr1): handr2,startIdx = removeCard(handr1, card2, startIdx) if card2 == WILD: #print "trying to start run with 2 WILD cards" for card3 in set(handr2): handr3,startIdx = removeCard(handr2, card3, startIdx) if card3 == WILD: yield (START_RUN, 3*[WILD], handr3, startIdx) else: try: card2v = prevInRun(card3) card1v = prevInRun(card2v) yield (START_RUN, [card1,card2,card3], handr3, startIdx) except ValueError: pass else: #print "trying to start run with WILD card and %s" % str(card2) try: card1v = prevInRun(card2) card3 = nextInRun(card2) #print "card3 = %s, handr2 = %s" % (str(card3),str(handr2)) canContinueRun = card3 in handr2 if not canContinueRun and WILD in handr2: card3 = WILD canContinueRun = True if canContinueRun: handr3,startIdx = removeCard(handr2, card3, startIdx) yield (START_RUN, [card1,card2,card3], handr3, startIdx) except ValueError: pass return # no need to start a set with a WILD else: try: card2v = card2 = nextInRun(card1) canContinueRun = card2 in handr1 #print "card2 = %s, handr1 = %s" % (str(card2),str(handr1)) if not canContinueRun and WILD in handr1: card2v = card2 card2 = WILD canContinueRun = True if canContinueRun: handr2,startIdx = removeCard(handr1, card2, startIdx) card3 = nextInRun(card2v) canContinueRun = card3 in handr2 #print "card3 = %s, handr2 = %s" % (str(card3),str(handr2)) if not canContinueRun and WILD in handr2: card3 = WILD canContinueRun = True if canContinueRun: handr3,startIdx = removeCard(handr2, card3, startIdx) yield (START_RUN, [card1,card2,card3], handr3, startIdx) except ValueError: pass handrs = copy.deepcopy(handr1) for card2 in set(handr1): handrs.remove(card2) if not fit2Set(card1,card2): continue handr2,startIdx = removeCard(handr1, card2, startIdx) for card3 in set(handrs): if not fit2Set(card1,card3): continue handr3,startIdx = removeCard(handr2, card3, startIdx) yield (START_SET, [card1,card2,card3], handr3, startIdx) def startings(hand,startIdx=0,runs=[],sets=[]): #print "get startings for\n hand = %s\n startIdx = %d\n runs = %s\n sets = %s" % (str(hand),startIdx,str(runs),str(sets)) if len(hand) == 0 or startIdx >= len(hand): yield copy.deepcopy(hand),copy.deepcopy(runs),copy.deepcopy(sets) return card = hand[startIdx] while hand.index(card) < startIdx: startIdx += 1 # do not try to start with the same card twice here if startIdx >= len(hand): yield copy.deepcopy(hand),copy.deepcopy(runs),copy.deepcopy(sets) return card = hand[startIdx] #print " actual startIdx = %d" % startIdx handr = removeCard(hand, card) for move in startMoves(handr, card, startIdx): if move[0] == START_RUN: runs2 = copy.deepcopy(runs) runs2.append(move[1]) for starting in startings(move[2], move[3], runs2, sets): yield starting elif move[0] == START_SET: sets2 = copy.deepcopy(sets) sets2.append(move[1]) for starting in startings(move[2], move[3], runs, sets2): yield starting for h,r,s in startings(hand, startIdx+1, runs, sets): yield h,r,s def runExtensions(run,handSet): if set(run) == set([WILD]): # run consists only of WILD cards for card in handSet: yield card else: runUnique = list(set(run)) cardv = runUnique[0] if runUnique[0] != WILD else runUnique[1] idx = run.index(cardv) lastCardVal = cardv while idx < len(run)-1: lastCardVal = nextInRun(lastCardVal) idx += 1 try: nextCardVal = nextInRun(lastCardVal) if nextCardVal in handSet: yield nextCardVal return if WILD in handSet: yield WILD except ValueError: pass def fillings(hand, runs, sets): if len(runs) > 0: run1 = runs[0] noExtensionFound = True usedWildExtension = False for runExtension in runExtensions(run1,set(hand)): noExtensionFound = False run1e = copy.deepcopy(run1) run1e.append(runExtension) handr = removeCard(hand, runExtension) runse = [run1e] + copy.deepcopy(runs[1:]) for filling in fillings(handr, runse, sets): yield filling if runExtension == WILD: usedWildExtension = True # when extending with WILD: also try without extending the first run; WILD card could be needed in another run if noExtensionFound or usedWildExtension and len(runs) > 1: for h,r,s in fillings(hand, copy.deepcopy(runs[1:]), sets): r = [run1] + r yield h,r,s return handr = copy.deepcopy(hand) setse = copy.deepcopy(sets) for card in hand: for set_i in setse: if fit2Set(card, set_i[0]): handr.remove(card) set_i.append(card) break yield handr,[],setse def valueCard(card): if card == WILD: return 20 return card[1] def value(hand): return sum([valueCard(card) for card in hand]) def strRepSuit(suit): if suit == CLUBS: return 'clubs' if suit == DIAMONDS: return 'diamonds' if suit == HEARTS: return 'hearts' if suit == SPADES: return 'spades' if suit == STARS: return 'stars' def strRepCard(card): if card == WILD: return 'WILD' return '%s%d' % (strRepSuit(card[0]), card[1]) def strRepCardSuit(card): if card == WILD: return 'WILD' return strRepSuit(card[0]) def strRepVal(card): if card == WILD: return 'WILD' return '%d' % card[1] def strRepRun(run): setRun = set(run) if setRun == set([WILD]): return '%d * %s' % (len(run),strRepCard(run[0])) runUnique = list(setRun) suit = (runUnique[0] if runUnique[0] != WILD else runUnique[1])[0] return '%s %s' % (strRepSuit(suit), ','.join([strRepVal(card) for card in run])) def strRepSet(set_): setUnique = list(set(set_)) val = (setUnique[0] if setUnique[0] != WILD else setUnique[1])[1] return '%d %s' % (val, ','.join([strRepCardSuit(card) for card in set_])) def strRepSol(hand,runs,sets): return ' runs: %s\n sets: %s\n hand: %s\n hand value: %d' % ('; '.join([strRepRun(run) for run in runs]), '; '.join([strRepSet(set_) for set_ in sets]), ','.join([strRepCard(card) for card in hand]), value(hand)) def optimizeHand(hand): lowestHandValue = value(hand) bestSolution = hand,[],[] for handr,runs,sets in startings(hand): #print "starting with\n runs = %s\n sets = %s\n handr = %s" % (str(runs),str(sets),str(handr)) if len(runs) == 0 and len(sets) == 0: continue if len(handr) == 0: print "solution generated without filling" lowestHandValue = 0 bestSolution = [],runs,sets break for solution in fillings(handr,runs,sets): handValue = value(solution[0]) if handValue < lowestHandValue: lowestHandValue = handValue bestSolution = solution print "solution improved:\n%s" % strRepSol(bestSolution[0], bestSolution[1], bestSolution[2]) if lowestHandValue == 0: break if lowestHandValue == 0: break print "best solution:\n%s" % strRepSol(bestSolution[0], bestSolution[1], bestSolution[2]) #return lowestHandValue, bestSolution
Now that the code is working on your example, we get the following output:
>>> optimizeHand(testHand) solution improved: runs: clubs 3,4,5,6; spaded WILD,WILD,10; clubs 6,7,8 sets: hand: spaded3,hearts10 hand value: 13 solution improved: runs: clubs 3,4,5,6; clubs WILD,7,8 sets: 10 spaded,WILD,hearts hand: spaded3,clubs6 hand value: 9 solution improved: runs: clubs 3,4,5,6; clubs WILD,6,7,8 sets: 10 spaded,WILD,hearts hand: spaded3 hand value: 3 solution generated without filling best solution: runs: clubs 4,5,6; clubs 6,7,8 sets: 3 clubs,WILD,spaded; 10 spaded,WILD,hearts hand: hand value: 0
The runtime is about 0.1 s on my computer.
The above code finds other evil solutions:
>>> optimizeHand([WILD,WILD,(CLUBS,3)]) ... runs: clubs 3,WILD,WILD >>> optimizeHand([WILD,WILD,(CLUBS,13)]) ... runs: clubs WILD,WILD,13 >>> optimizeHand([WILD,WILD,(CLUBS,13),(CLUBS,11)]) ... runs: clubs WILD,11,WILD,13