I recently posted a solution for the numbers game on the Countdown TV show using a recursive technique. For those unfamiliar with the rules...
- A fixed set of numbers (usually 6) are chosen at random.
- A target value is set.
- The player must reach the target value using some or all of the numbers from the list of 6 random numbers using only the operators +, ✕, - and /. The use of brackets is also permitted.
I have attached a new program that uses the itertools
library and for
loops. As with the last code the print
statement
- "Eval..." means the program has used BODMAS (e.g: 1+3✕10 = 31 as 10✕3 = 30 and 30+1 = 31) whereas
- "Running sum..." produces a running sum of the totals (1+3✕10 = 40 as 1+3 = 4, 4✕10 = 40)
import itertools
import re
def unpack(method):
string = method
special = ["*","+","-","/"]
list_sum = []
list_special = []
numbers = (re.findall(r"[\w']+", string))
for char in string:
if char in special:
list_special.append(char)
for index in range (len(numbers)-1):
to_eval = numbers[index] + list_special[index] + numbers[index+1]
list_sum.append(f'{to_eval} = {eval(to_eval)}')
numbers[index+1] = str(eval(to_eval))
return list_sum
def plus (x,y): return x+y, "+"
def minus (x,y): return x-y, "-"
def times (x,y): return x*y, "*"
def divide (x,y): return x/y, "/"
def solve():
for numbers in list_sols:
total = 0
running = ""
list_to_call = [order_ops1,order_ops2,order_ops3,order_ops4,order_ops5,order_ops6]
for operator in list_to_call[len(numbers)-2]:
zipped = itertools.zip_longest(numbers, operator)
to_calculate = (list(zipped))
running = ""
total = 0
for (index,item) in enumerate(to_calculate):
if index == 0:
total += item[0]
next_operator = item[1]
running = str(item[0])
else:
total, symbol = next_operator(total,item[0])
running += symbol + str(item[0])
if index < len(numbers)-1: next_operator = item[1]
if eval(running)==target:
print(f"Eval: {running}")
break
if total == target:
print (f'Running sum: {unpack(running)}')
break
def main():
global list_sols
global order_ops1, order_ops2, order_ops3, order_ops4, order_ops5, order_ops6
global target
numbers = [100,50,75,25,3,6]
target = 952
list_sols = []
for number in itertools.chain.from_iterable(itertools.permutations(numbers,i) for i in range(2,7)):
list_sols.append(list(number))
order_ops1,order_ops2,order_ops3,order_ops4,order_ops5,order_ops6 = [],[],[],[],[],[]
for number in itertools.chain.from_iterable(itertools.product([plus,minus,times,divide],repeat=i) for i in range(1,6)):
if len(number) ==1: order_ops1.append(list(number))
if len(number) ==2: order_ops2.append(list(number))
if len(number) ==3: order_ops3.append(list(number))
if len(number) ==4: order_ops4.append(list(number))
if len(number) ==5: order_ops5.append(list(number))
if len(number) ==6: order_ops6.append(list(number))
if target in numbers:
print (target)
return
else:
solve()
return
if __name__=="__main__":
main()
exit_key = input("Press any key to exit...")
-
\$\begingroup\$ If you like this, you might be interested in reviewing my C++ implementation of a similar problem: Find an arithmetic expression near to target value \$\endgroup\$Toby Speight– Toby Speight2025年03月19日 09:24:38 +00:00Commented Mar 19 at 9:24
2 Answers 2
Layout
This line in the solve
function:
if index < len(numbers)-1: next_operator = item[1]
is more customarily split into 2 lines:
if index < len(numbers)-1:
next_operator = item[1]
There are other instances of this elsewhere in the code.
Documentation
The PEP 8 style guide recommends adding docstrings for functions. The docstring should summarize what the function is doing and the types of its inputs and return values.
Naming
main
is a bit generic for a function name. Perhaps numbers_game
would be more meaningful.
DRY
In main
, the expression len(number)
is repeated several times. You can
set it to a variable.
The 6 separate if
statements in the for
loop:
if len(number) ==1: order_ops1.append(list(number))
if len(number) ==2: order_ops2.append(list(number))
should be combined into a single if/elif
statement:
num_len = len(number)
if num_len == 1:
order_ops1.append(list(number))
elif num_len == 2:
order_ops2.append(list(number))
The checks are mutually exclusive. This makes the code more efficient since you don't have to perform the 2nd check if the first is true, etc. Also, this more clearly shows the intent of the code.
The return
line is used in each branch in this if/else
:
if target in numbers:
print (target)
return
else:
solve()
return
It can be moved after the if/else
:
if target in numbers:
print (target)
else:
solve()
return
Kudos on the four very nice arithmetic ops that return tuples. Of course we already have operators, but they lack a display glyph.
appropriate data structure
Identifiers like
order_ops6
suggest that we want a six element array.
globals
Just say no to the global
keyword.
It seldom improves maintainability or readability.
Prefer to parameterize your function, or perhaps introduce a class that has self
attributes.