I am trying to program a solution generator for the Countdown numbers game in Python, following some guidance from DataGenetics -- those unfamiliar with the rules, you can review them here as well -- but I wrote the code myself.
Code
def postfix_to_infix(postfix):
"""
:param postfix: tuple containing solution in reverse polish notation
:return: string of solution in polish notation
"""
soln = list(postfix)
while True:
try:
op_idx, op = [(idx, tkn) for idx, tkn in enumerate(soln) if tkn in ['+', '-', '*', '/']][0]
except IndexError:
break
else:
block = '(' + str(soln[op_idx - 2]) + op + str(soln[op_idx - 1]) + ')'
soln[op_idx - 2:op_idx + 1] = [''.join(block)]
return soln
def main():
import random
# Print welcome message
print("""
=====================================
Welcome to Countdown, Python version!
=====================================
""")
# *** <Start game> ***
play_new_game = True
while play_new_game:
# Generate the set of possible numbers allowed by Countdown rules
large_numbers = [25, 50, 75, 100]
small_numbers = list(range(1, 11)) * 2
# Generate six number tiles according to user input for large numbers
while True:
try:
usr_large_num = int(input("How many LARGE numbers do you want (0-4)? "))
except ValueError:
pass
else:
if 0 <= usr_large_num <= 4:
break
six_tiles = random.sample(large_numbers, usr_large_num) + \
random.sample(small_numbers, 6 - usr_large_num)
# Generate random target output: any three-digit number
target = random.randint(100, 999)
print(six_tiles)
print(f"Target: {target}")
# Algorithm to find solution to target
import itertools as it
from more_itertools import distinct_permutations
import operator
ops_tiles = {"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv}
for nums in distinct_permutations(six_tiles):
for ops in it.combinations_with_replacement(ops_tiles, 4):
# Generate the list of postfix's (6 numbers + operators)
# by permuting over all elements,
# But removing those postfix's that begin with two operators
postfix_gen = (pf for pf in distinct_permutations(nums + ops) if
pf[0] not in ops_tiles and pf[1] not in ops_tiles)
for postfix in postfix_gen:
# Implement postfix algorithm
stack = list(postfix[0:2])
for idx, token in enumerate(postfix[2:], start=2):
# If token is an operator...
if token in ops_tiles:
# Take the last 2 tokens in the stack
try:
operand_2 = stack.pop()
operand_1 = stack.pop()
except IndexError:
break
# And operate on the 2 tokens
else:
# Capture ZeroDivisionError
try:
result = ops_tiles[token](operand_1, operand_2)
except ZeroDivisionError:
break
else:
# Intermediate numbers can only be whole numbers
if (result < 0) or (result != int(result)):
break
elif result == target:
postfix = postfix[0:idx + 1]
break
else:
stack.append(result)
# Else token is a number
else:
# Add number to the stack and await an operator
stack.append(token)
if result == target:
break
else:
continue
break
else:
continue
break
# Print results!
if result != target:
print("No solution found!")
else:
print(postfix)
print(postfix_to_infix(postfix))
# Play again?
while True:
try:
print("Do you want to play again (y/n)?")
play_again = input()[0]
except IndexError:
pass
else:
if play_again.lower() == 'n':
play_new_game = False
break
elif play_again.lower() == 'y':
print("\n==========")
break
if __name__ == '__main__':
main()
A short explanation
The above code does the following things:
- As per the game rules, the player is asked how many 'big' numbers they want (between 0 to 4), and then the 6 numbers are picked out at random (the remaining being 'small' numbers) and printed.
- The target number (random number between 100 to 999) is printed out as well.
- It tries to find a solution via brute force (iterating through all possible combinations of numbers and operators to achieve the target number). I use Reverse Polish notation in my solution-searching algorithm.
- Print the solution, if available, and do so in (standard) Polish notation. The conversion from RPN to PN is via the function
postfix_to_infix
function.
Notes and Questions
Just for context: I'm relatively new to Python and OOP in general; I have a better grasp of functional programming. I would gladly appreciate some feedback regarding:
- How to make the code more efficient? Currently it takes a very, very long time to report a given combination has no solution. NB: It will be even worse if I allow for more operators in the
postfix
expression (>4). As such, I am not 100% sure that this code will find the solutions for all possible games, but I can only increase the number of operators once my code runs more efficiently -- otherwise it would lengthen the time to even find solutions of extremely easy games (e.g. 100, 25, 2, 4, 6, 8 with target 650). - There are indeed some efficiency checks suggested in the link above (by DataGenetics) that I did not implement in my code as I'm not too sure yet (e.g. steps that multiply and divide by 1 -- wasting moves)
- Would it be possible to implement a class for this? I'm not sure what the objects will be in this case. I'll also be willing to hear why it isn't worth implementing this in a class, if you feel this way (I suspect this is the case here).
- Any improvements for the
postfix_to_infix
function and the way it is implemented in Python?
Thank you!