~dricottone/numberplay

443711356cdfba08557b5d37dc6f6684c1f1f451 — Dominic Ricottone 2 years ago dev
Solution
4 files changed, 400 insertions(+), 0 deletions(-)

A .gitignore
A Makefile
A main.py
A test.py
A  => .gitignore +1 -0
@@ 1,1 @@
__pycache__

A  => Makefile +12 -0
@@ 1,12 @@
.PHONY: clean
clean:
	rm -rf __pycache__

.PHONY: test
test:
	python -m unittest --verbose

.PHONY: solve
solve:
	python -m main


A  => main.py +216 -0
@@ 1,216 @@
#!/usr/bin/env python3

"""NUMBERPLAY
Given
 (1) N != U != M != B != E != R != P != L != A != Y
 (2) N != B != P != 0
Find all valid solutions to NUM + BER = PLAY
"""

class Solution(object):
	def __init__(
		self,
		*,
		n=None,
		u=None,
		m=None,
		b=None,
		e=None,
		r=None,
		p=None,
		l=None,
		a=None,
		y=None,
		carry=0
	):
		self._validate("n", n, allow_none=True)
		self.n = n
		self._validate("u", u, allow_none=True)
		self.u = u
		self._validate("m", m, allow_none=True)
		self.m = m
		self._validate("b", b, allow_none=True)
		self.b = b
		self._validate("e", e, allow_none=True)
		self.e = e
		self._validate("r", r, allow_none=True)
		self.r = r
		self._validate("p", p, allow_none=True)
		self.p = p
		self._validate("l", l, allow_none=True)
		self.l = l
		self._validate("a", a, allow_none=True)
		self.a = a
		self._validate("y", y, allow_none=True)
		self.y = y
		if carry not in [0,1]:
			raise ValueError("carry must be 0 or 1")
		self.carry = carry

	def __str__(self):
		_n = str(self.n) if self.n is not None else "N"
		_u = str(self.u) if self.u is not None else "U"
		_m = str(self.m) if self.m is not None else "M"
		_b = str(self.b) if self.b is not None else "B"
		_e = str(self.e) if self.e is not None else "E"
		_r = str(self.r) if self.r is not None else "R"
		_p = str(self.p) if self.p is not None else "P"
		_l = str(self.l) if self.l is not None else "L"
		_a = str(self.a) if self.a is not None else "A"
		_y = str(self.y) if self.y is not None else "Y"
		return f"{_n}{_u}{_m} + {_b}{_e}{_r} = {_p}{_l}{_a}{_y}"

	def __getitem__(self, key):
		if key == "n":
			return self.n
		elif key == "u":
			return self.u
		elif key == "m":
			return self.m
		elif key == "b":
			return self.b
		elif key == "e":
			return self.e
		elif key == "r":
			return self.r
		elif key == "p":
			return self.p
		elif key == "l":
			return self.l
		elif key == "a":
			return self.a
		elif key == "y":
			return self.y
		else:
			raise AttributeError(f"no position '{key}'")

	def __setitem__(self, key, value):
		if self[key] is not None:
			raise AttributeError(f"position '{key}' is already set")

		self._validate(key, value)

		if key == "n":
			self.n = value
		elif key == "u":
			self.u = value
		elif key == "m":
			self.m = value
		elif key == "b":
			self.b = value
		elif key == "e":
			self.e = value
		elif key == "r":
			self.r = value
		elif key == "p":
			self.p = value
		elif key == "l":
			self.l = value
		elif key == "a":
			self.a = value
		elif key == "y":
			self.y = value
		else:
			raise AttributeError(f"no position '{key}'")

	def copy(self):
		"""Duplicate this solution."""
		return Solution(n=self.n, u=self.u, m=self.m, b=self.b, e=self.e,
			r=self.r, p=self.p, l=self.l, a=self.a, y=self.y, carry=self.carry)

	def is_using(self, n):
		"""Check if n is already used in the solution."""
		if not isinstance(n, int) or n < 0 or 9 < n:
			raise ValueError("values must be integers between 0 and 9")

		used = []
		for position in ["n", "u", "m", "b", "e", "r", "p", "l", "a", "y"]:
			try:
				used.append(self[position])
			except AttributeError:
				pass
	
		return n in used

	def _validate(self, position, value, allow_none=False):
		"""Validate a value for an position."""
		if not isinstance(value, int):
			if allow_none == False:
				raise ValueError("value must be an integer")
			elif value is not None:
				raise ValueError("value must be an integer or None")
		else:
			if value < 0 or 9 < value:
				raise ValueError("value must be an integer between 0 and 9")
			elif position in ["n", "b", "p"] and value == 0:
				raise ValueError("value must be an integer between 1 and 9")
			elif self.is_using(value):
				raise ValueError("value must be unique")

def add_digits(a, b, carry):
	sum = a + b + carry
	new_carry = (10 <= sum)
	c = int(str(sum)[-1])
	return new_carry, c

def solve_column(partial, r1, r2, r3):
	solutions = []

	for a in range(0,10):
		if partial.is_using(a):
			continue
		elif r1 == "n" and a == 0:
			continue

		for b in range(0,10):
			if a == b or partial.is_using(b):
				continue
			elif r2 == "b" and b == 0:
				continue

			new_carry, c = add_digits(a, b, partial.carry)
			if a == c or b == c or partial.is_using(c):
				continue

			new_solution = partial.copy()
			new_solution[r1] = a
			new_solution[r2] = b
			new_solution[r3] = c
			new_solution.carry = new_carry
			solutions.append(new_solution)

	return solutions

def main():
	count = 0

	# P = 1 (no values satisfy a + b >= 2000 where a, b <= 999)
	solution = Solution(p=1)

	# solve right column   N U *
	#                    + B E *
	#                    -------
	#                    P L A *
	for s1 in solve_column(solution, 'm', 'r', 'y'):

		# solve middle column   N * M
		#                     + B * R
		#                     -------
		#                     P L * Y
		for s2 in solve_column(s1, 'u', 'e', 'a'):

			# solve left column   * U M
			#                   + * E R
			#                   -------
			#                   P * A Y
			for s3 in solve_column(s2, 'n', 'b', 'l'):

				# now just filter to solutions that carry a 1 to P
				if s3.carry == 1:
					print(s3)
					count += 1

	print(f"Total: {count} solutions")

if __name__ == "__main__":
	main()

A  => test.py +171 -0
@@ 1,171 @@
import unittest

import main

class TestAddDigits(unittest.TestCase):
	"""add_digits(a, b, carry)"""

	def test_simple_add(self):
		# only test 0-8, since adding 1 to 9 would carry
		for a in range(0, 9):
			with self.subTest(a=a):
				self.assertEqual(main.add_digits(a, 1, 0), (0, a+1, ))
	
	def test_carry(self):
		# only test 1-9, since adding 0 to 9 would not carry
		for a in range(1,10):
			with self.subTest(a=a):
				self.assertEqual(main.add_digits(a, 9, 0)[0], 1)

	def test_large_add(self):
		# only test 1-9, since this test uses a-1
		for a in range(1,10):
			with self.subTest(a=a):
				self.assertEqual(main.add_digits(a, 9, 0)[1], a-1)

	def test_large_add_alt(self):
		for a in range(0,10):
			with self.subTest(a=a):
				self.assertEqual(main.add_digits(a, 9, 1)[1], a)

class TestSolution(unittest.TestCase):
	"""Solution()"""

	def test_str(self):
		s = main.Solution(n=1, u=2, m=3, b=4, e=5, r=6, p=7, l=8, a=9, y=0)
		self.assertEqual(str(s), "123 + 456 = 7890")

	def test_copy(self):
		s = main.Solution(n=1)
		c = s.copy()
		c['u'] = 2
		self.assertEqual(s.u, None)
		self.assertEqual(c.u, 2)

	def test_get_wrong_name(self):
		with self.assertRaises(AttributeError):
			main.Solution()['x']

	def test_set_wrong_name(self):
		with self.assertRaises(AttributeError):
			main.Solution()['x'] = 1

	def test_set_wrong_name_wrong_value(self):
		with self.assertRaises(AttributeError):
			main.Solution()['x'] = None

	def test_raise_leading_0(self):
		with self.subTest(a="n"):
			with self.assertRaises(ValueError):
				main.Solution(n=0)
		with self.subTest(a="b"):
			with self.assertRaises(ValueError):
				main.Solution(b=0)
		with self.subTest(a="p"):
			with self.assertRaises(ValueError):
				main.Solution(p=0)

		with self.assertRaises(ValueError):
			main.Solution(m=10)

	def test_raise_10(self):
		with self.assertRaises(ValueError):
			main.Solution(m=10)

	def test_raise_neg1(self):
		with self.assertRaises(ValueError):
			main.Solution(m=-1)

	def test_raise_set_leading_0(self):
		for a in ["n", "b", "p"]:
			with self.subTest(a=a):
				with self.assertRaises(ValueError):
					main.Solution()[a] = 0

	def test_raise_set_10(self):
		with self.assertRaises(ValueError):
			main.Solution()["m"] = 10

	def test_raise_set_neg1(self):
		with self.assertRaises(ValueError):
			main.Solution()["m"] = -1

	def test_raise_set_none(self):
		with self.assertRaises(ValueError):
			main.Solution()["m"] = None

class TestIsUsed(unittest.TestCase):
	"""Solution().is_used(n)"""

	def test_raise_10(self):
		with self.assertRaises(ValueError):
			main.Solution().is_using(10)

	def test_raise_neg1(self):
		with self.assertRaises(ValueError):
			main.Solution().is_using(-1)

	def test_empty_solution(self):
		for a in range(0, 10):
			with self.subTest(a=a):
				self.assertFalse(main.Solution().is_using(a))

	def test_unused(self):
		# only test 0-8, since this test uses a+1
		for a in range(0, 9):
			with self.subTest(a=a):
				self.assertFalse(main.Solution(m=a+1).is_using(a))

	def test_unused_alt(self):
		# only test 1-9, since this test uses a-1
		for a in range(1, 10):
			with self.subTest(a=a):
				self.assertFalse(main.Solution(m=a-1).is_using(a))

	def test_copy_unused(self):
		# only test 0-8, since this test uses a+1
		for a in range(0, 9):
			with self.subTest(a=a):
				s = main.Solution(m=a+1)
				self.assertFalse(s.copy().is_using(a))

	def test_updated_copy_unused(self):
		# only test 0-7, since this test uses a+1 and a+2
		for a in range(0, 8):
			with self.subTest(a=a):
				s = main.Solution(m=a+1)
				c = s.copy()
				c['r'] = a+2
				self.assertFalse(c.is_using(a))

	def test_used(self):
		for a in range(0, 10):
			with self.subTest(a=a):
				self.assertTrue(main.Solution(m=a).is_using(a))

	def test_copy_used(self):
		for a in range(0, 10):
			with self.subTest(a=a):
				s = main.Solution(m=a)
				self.assertTrue(s.copy().is_using(a))

	def test_updated_copy_used(self):
		# only test 0-8, since this test uses a+1
		for a in range(0, 9):
			with self.subTest(a=a):
				s = main.Solution(m=a+1)
				c = s.copy()
				c['r'] = a
				self.assertTrue(c.is_using(a))

class TestCase1(unittest.TestCase):
	def test_case(self):
		"""N32 + B57 = 1L89"""
		s = main.Solution(u=3, m=2, e=5, r=7, p=1, a=8, y=9)
		self.assertEqual(str(s), "N32 + B57 = 1L89")
		result = main.solve_column(s, 'n', 'b', 'l')
		should_be = ["432 + 657 = 1089","632 + 457 = 1089"]
		self.assertEqual([str(r) for r in result], should_be)

if __name__ == '__main__':
	unittest.main()