├── tests ├── __init__.py ├── context.py └── test_integer.py ├── tchisla ├── __init__.py ├── collection.py ├── solution.py ├── operator.py ├── generation.py ├── records.py └── cheater.py ├── README.md ├── test.py ├── cycle.py ├── solve.py ├── refresh_cache.py ├── update_results.sh ├── export_csv.py ├── get_records.ps1 ├── cheater.py ├── .gitignore ├── remote_checker.py ├── check_gs_records.py ├── wr_checker.py └── range_cheater.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tchisla/__init__.py: -------------------------------------------------------------------------------- 1 | from collection import * 2 | from cheater import * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tchisla 2 | Generate solutions for the game [Tchisla](https://itunes.apple.com/us/app/tchisla/id1100623105) -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 6 | import tchisla as t 7 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import sympy 2 | import tchisla as t 3 | 4 | collection = t.Collection(6) 5 | collection.build(5) 6 | for solution in collection.iter_solution(): 7 | print sympy.Rational(357)/solution.number 8 | -------------------------------------------------------------------------------- /cycle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | os.system("./refresh_cache.py") 6 | os.system("./range_cheater.py 1 10000 1 9 skip") 7 | os.system("./refresh_cache.py") 8 | os.system("./wr_checker.py 1 10000 1 9 > results/wr_checker.txt") 9 | -------------------------------------------------------------------------------- /solve.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import tchisla as t 3 | 4 | def main(): 5 | filename, base, limit = sys.argv 6 | collection = t.Collection(int(base)) 7 | collection.build(int(limit)) 8 | collection.output(None) 9 | 10 | if __name__ == '__main__': 11 | main() 12 | -------------------------------------------------------------------------------- /refresh_cache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import tchisla as t 5 | 6 | if not os.path.exists(t.records.tmp_dir): 7 | os.makedirs(t.records.tmp_dir) 8 | 9 | 10 | if os.path.exists(t.records.cache_path): 11 | os.remove(t.records.cache_path) 12 | 13 | t.records.load() 14 | -------------------------------------------------------------------------------- /tests/test_integer.py: -------------------------------------------------------------------------------- 1 | from context import * 2 | 3 | 4 | class TestStringMethods(unittest.TestCase): 5 | def test_1(self): 6 | collection = t.Collection(1) 7 | collection.build(5) 8 | registry = t.Solution.registry 9 | self.assertEqual(registry[4].complexity, 4) 10 | self.assertEqual(registry[5].complexity, 4) 11 | self.assertEqual(registry[6].complexity, 3) 12 | 13 | 14 | if __name__ == '__main__': 15 | unittest.main() 16 | -------------------------------------------------------------------------------- /update_results.sh: -------------------------------------------------------------------------------- 1 | ./range_cheater.py 1 10000 1 1 > results/range_cheater/1.txt 2 | ./range_cheater.py 1 10000 2 2 > results/range_cheater/2.txt 3 | ./range_cheater.py 1 10000 3 3 > results/range_cheater/3.txt 4 | ./range_cheater.py 1 10000 4 4 > results/range_cheater/4.txt 5 | ./range_cheater.py 1 10000 5 5 > results/range_cheater/5.txt 6 | ./range_cheater.py 1 10000 6 6 > results/range_cheater/6.txt 7 | ./range_cheater.py 1 10000 7 7 > results/range_cheater/7.txt 8 | ./range_cheater.py 1 10000 8 8 > results/range_cheater/8.txt 9 | ./range_cheater.py 1 10000 9 9 > results/range_cheater/9.txt 10 | -------------------------------------------------------------------------------- /export_csv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import csv 5 | import tchisla as t 6 | 7 | all_records = t.records.load() 8 | records_to_export =[record for record in all_records if int(record['target']) <= 10000] 9 | fieldnames = ['id', 'target', 'digits', 'digits_count', 'creation_date', 'update_date'] 10 | 11 | print len(records_to_export) 12 | 13 | with open('results/tchisla_records.csv', 'wb') as csvfile: 14 | writer = csv.writer(csvfile) 15 | writer.writerow(fieldnames) 16 | for record in records_to_export: 17 | row = [record[fieldname] for fieldname in fieldnames] 18 | writer.writerow(row) 19 | -------------------------------------------------------------------------------- /get_records.ps1: -------------------------------------------------------------------------------- 1 | # $url = 'http://httpbin.org/json' 2 | $url = 'http://www.euclidea.xyz/api/v1/game/numbers/solutions/records' 3 | $r = Invoke-WebRequest $url 4 | $r.content >> ./result.json 5 | 6 | $results = Get-Content .\result.json | ConvertFrom-Json 7 | $records = $results.records 8 | $records.Length 9 | 10 | $records6 = $records | Where-Object {$_.digits -eq 6} 11 | $records6.Length 12 | 13 | $recentRecords6 = $records6 | Where-Object {$_.update_date -gt '2020-03-20T17:11:03.504Z'} 14 | 15 | $recentRecords6.Length 16 | 17 | $recentRecords6 | Sort-Object -Property update_date > ./recentRecords6.txt 18 | 19 | -------------------------------------------------------------------------------- /tchisla/collection.py: -------------------------------------------------------------------------------- 1 | import sympy 2 | from generation import * 3 | 4 | 5 | class Collection(): 6 | """Holds all solutions for all numbers with the same base""" 7 | def __init__(self, base): 8 | self.base = base 9 | 10 | def build(self, generation_limit=4): 11 | for generation_index in xrange(1, generation_limit): 12 | print "{}/{}".format(generation_index, generation_limit - 1) 13 | generation = Generation(generation_index, self.base) 14 | generation.build() 15 | 16 | def output(self, number_limit=100): 17 | for solution in self.iter_solution(number_limit): 18 | print solution.formatted() 19 | 20 | def iter_solution(self, number_limit=100): 21 | sorted_keys = sorted(Solution.registry.keys(), key=lambda x: sympy.N(x)) 22 | for key in sorted_keys: 23 | if number_limit is None or key <= number_limit: 24 | yield Solution.registry[key] 25 | -------------------------------------------------------------------------------- /cheater.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import tchisla as t 5 | 6 | if len(sys.argv) == 4: 7 | filename, target_string, digit_string, digits_count_string = sys.argv 8 | 9 | final_target = int(target_string) 10 | final_digit = int(digit_string) 11 | final_digits_count = int(digits_count_string) 12 | elif len(sys.argv) == 3: 13 | filename, target_string, digit_string = sys.argv 14 | 15 | final_target = int(target_string) 16 | final_digit = int(digit_string) 17 | final_digits_count = None 18 | 19 | records = t.records.get(final_digit) 20 | 21 | if final_target not in records and final_digits_count is None: 22 | print "No record for {}#{}".format(final_target, final_digit) 23 | else: 24 | if final_digits_count is None: 25 | final_digits_count = records[final_target] 26 | 27 | print "{}#{} ({}):".format(final_target, final_digit, final_digits_count) 28 | solution = t.cheater.solve(final_target, final_digits_count, records, fail_fast=True) 29 | print solution 30 | # if solution is not None: 31 | # t.records.submit(final_target, final_digit, final_digits_count, solution) 32 | -------------------------------------------------------------------------------- /tchisla/solution.py: -------------------------------------------------------------------------------- 1 | class Solution(): 2 | """Solution for number # base""" 3 | registry = {} 4 | known_improvements = { 5 | 2: { 6 | 3: [64] 7 | }, 8 | 8: { 9 | 3: [1024], 10 | 5: [50] 11 | } 12 | } 13 | 14 | def __init__(self, number, base, complexity, operator=None, input_1=None, input_2=None): 15 | self.number = number 16 | self.base = base 17 | self.input_1 = input_1 18 | self.input_2 = input_2 19 | self.operator = operator 20 | self.complexity = complexity 21 | 22 | def formatted(self): 23 | formula = '' 24 | 25 | if self.input_1 is None: 26 | formula = '' 27 | elif self.input_2 is None: 28 | if self.operator == '!': 29 | formula = '{} {}'.format(self.input_1.number, self.operator) 30 | else: 31 | formula = '{} {}'.format(self.operator, self.input_1.number) 32 | else: 33 | formula = '{} {} {}'.format(self.input_1.number, self.operator, self.input_2.number) 34 | 35 | return '{}: {}, {}'.format(self.number, self.complexity, formula) 36 | 37 | @classmethod 38 | def register(cls, solution): 39 | # only register the first time because the complexity is minimum 40 | if solution.number not in cls.registry: 41 | cls.registry[solution.number] = solution 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | results/* 92 | tmp/ 93 | 94 | !results/range_cheater/ 95 | 96 | .DS_Store -------------------------------------------------------------------------------- /remote_checker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import tchisla as t 5 | 6 | if len(sys.argv) == 2: 7 | filename, target_lower_bound_string = sys.argv 8 | target_upper_bound_string = target_lower_bound_string 9 | digit_lower_bound_string = '1' 10 | digit_upper_bound_string = '9' 11 | elif len(sys.argv) == 3: 12 | filename, target_lower_bound_string, target_upper_bound_string = sys.argv 13 | digit_lower_bound_string = '1' 14 | digit_upper_bound_string = '9' 15 | elif len(sys.argv) == 4: 16 | filename, target_lower_bound_string, target_upper_bound_string, digit_lower_bound_string = sys.argv 17 | digit_upper_bound_string = digit_lower_bound_string 18 | elif len(sys.argv) == 5: 19 | filename, target_lower_bound_string, target_upper_bound_string, digit_lower_bound_string, digit_upper_bound_string = sys.argv 20 | 21 | target_lower_bound = int(target_lower_bound_string) 22 | target_upper_bound = int(target_upper_bound_string) 23 | final_digit_lower_bound = int(digit_lower_bound_string) 24 | final_digit_upper_bound = int(digit_upper_bound_string) 25 | 26 | api_records = t.records.get_api_records() 27 | 28 | unsolved_count = 0 29 | unknown_count = 0 30 | 31 | for digits in xrange(final_digit_lower_bound, final_digit_upper_bound + 1): 32 | print "Processing #{}".format(digits) 33 | 34 | for target in xrange(target_lower_bound, target_upper_bound + 1): 35 | if target % 1000 == 0: 36 | print target 37 | remote_records = api_records[digits] 38 | 39 | result = t.cheater.solve(target, remote_records[target] - 1, remote_records, fail_fast=True, suppress_failure=True) 40 | if result != None: 41 | print result 42 | 43 | -------------------------------------------------------------------------------- /tchisla/operator.py: -------------------------------------------------------------------------------- 1 | import math 2 | # from sympy import * 3 | 4 | 5 | class Operator(object): 6 | unary = ['sqrt', '!'] 7 | binary = ['+', '-', '*', '/', '^'] 8 | 9 | @classmethod 10 | def apply_unary(cls, operator, input_1): 11 | def sqrt(input): 12 | output = math.sqrt(input) 13 | if output % 1 == 0: 14 | return int(output) 15 | else: 16 | return None 17 | 18 | def factorial(input): 19 | if input < 30: 20 | return math.factorial(input) 21 | else: 22 | return None 23 | 24 | return { 25 | 'sqrt': sqrt, 26 | '!': factorial 27 | }[operator](input_1) 28 | 29 | @classmethod 30 | def apply_binary(cls, operator, input_1, input_2): 31 | def add(input_1, input_2): 32 | return input_1 + input_2 33 | 34 | def subtract(input_1, input_2): 35 | result = input_1 - input_2 36 | if result > 0: 37 | return result 38 | else: 39 | return None 40 | 41 | def times(input_1, input_2): 42 | return input_1 * input_2 43 | 44 | def divide(input_1, input_2): 45 | if input_2 == 0: 46 | return None 47 | if input_1 % input_2 == 0: 48 | return input_1 / input_2 49 | else: 50 | return None 51 | 52 | def power(input_1, input_2): 53 | if input_2 > 100: 54 | return None 55 | result = input_1 ** input_2 56 | if result < 10 ** 41: 57 | return result 58 | else: 59 | return None 60 | 61 | return { 62 | '+': add, 63 | '-': subtract, 64 | '*': times, 65 | '/': divide, 66 | '^': power 67 | }[operator](input_1, input_2) 68 | -------------------------------------------------------------------------------- /check_gs_records.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import tchisla as t 5 | 6 | if len(sys.argv) == 2: 7 | filename, target_lower_bound_string = sys.argv 8 | target_upper_bound_string = target_lower_bound_string 9 | digit_lower_bound_string = '1' 10 | digit_upper_bound_string = '9' 11 | elif len(sys.argv) == 3: 12 | filename, target_lower_bound_string, target_upper_bound_string = sys.argv 13 | digit_lower_bound_string = '1' 14 | digit_upper_bound_string = '9' 15 | elif len(sys.argv) == 4: 16 | filename, target_lower_bound_string, target_upper_bound_string, digit_lower_bound_string = sys.argv 17 | digit_upper_bound_string = digit_lower_bound_string 18 | elif len(sys.argv) == 5: 19 | filename, target_lower_bound_string, target_upper_bound_string, digit_lower_bound_string, digit_upper_bound_string = sys.argv 20 | 21 | target_lower_bound = int(target_lower_bound_string) 22 | target_upper_bound = int(target_upper_bound_string) 23 | final_digit_lower_bound = int(digit_lower_bound_string) 24 | final_digit_upper_bound = int(digit_upper_bound_string) 25 | 26 | 27 | gs_records = t.records.get_gs_records() 28 | api_records = t.records.get_all(merge_gs_records=False) 29 | 30 | for digits in xrange(final_digit_lower_bound, final_digit_upper_bound + 1): 31 | print "#{}:".format(digits) 32 | 33 | for target in xrange(target_lower_bound, target_upper_bound + 1): 34 | api_sub_records = api_records[digits] 35 | gs_sub_records = gs_records[digits] 36 | 37 | if target not in gs_sub_records: 38 | print "No GS record for {}#{}".format(target, digits) 39 | elif target not in api_sub_records: 40 | # API records are known to be incomplete 41 | pass 42 | else: 43 | if api_sub_records[target] != gs_sub_records[target]: 44 | print "{}#{}: API: {}; GS: {}".format(target, digits, api_sub_records[target], gs_sub_records[target]) 45 | print 46 | -------------------------------------------------------------------------------- /tchisla/generation.py: -------------------------------------------------------------------------------- 1 | from solution import * 2 | from operator import * 3 | 4 | 5 | class Generation(): 6 | generations = {} 7 | 8 | def __init__(self, index, base): 9 | self.index = index 10 | self.base = base 11 | self.solutions = set() 12 | 13 | def build(self): 14 | single_number = (10 ** self.index - 1) / 9 * self.base 15 | first_solution = Solution(single_number, self.base, self.index) 16 | 17 | seeds = [first_solution] 18 | if self.base in Solution.known_improvements: 19 | if self.index in Solution.known_improvements[self.base]: 20 | numbers = Solution.known_improvements[self.base][self.index] 21 | for number in numbers: 22 | seeds.append(Solution(number, self.base, self.index)) 23 | 24 | for input_1_index in xrange(1, self.index): 25 | input_2_index = self.index - input_1_index 26 | for input_1 in Generation.generations[input_1_index].solutions: 27 | for input_2 in Generation.generations[input_2_index].solutions: 28 | for operator in Operator.binary: 29 | result = Operator.apply_binary(operator, input_1.number, input_2.number) 30 | if (result is not None) and (result not in Solution.registry): 31 | new_solution = Solution(result, self.base, input_1.complexity + input_2.complexity, operator, input_1, input_2) 32 | seeds.append(new_solution) 33 | Solution.register(new_solution) 34 | 35 | for seed in seeds: 36 | Solution.register(seed) 37 | 38 | for operator in Operator.unary: 39 | result = Operator.apply_unary(operator, seed.number) 40 | if (result is not None) and (result not in Solution.registry): 41 | new_solution = Solution(result, self.base, seed.complexity, operator, seed) 42 | seeds.append(new_solution) 43 | Solution.register(new_solution) 44 | 45 | self.solutions = set(seeds) 46 | Generation.generations[self.index] = self 47 | -------------------------------------------------------------------------------- /wr_checker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import tchisla as t 5 | 6 | if len(sys.argv) == 2: 7 | filename, target_lower_bound_string = sys.argv 8 | target_upper_bound_string = target_lower_bound_string 9 | digit_lower_bound_string = '1' 10 | digit_upper_bound_string = '9' 11 | elif len(sys.argv) == 3: 12 | filename, target_lower_bound_string, target_upper_bound_string = sys.argv 13 | digit_lower_bound_string = '1' 14 | digit_upper_bound_string = '9' 15 | elif len(sys.argv) == 4: 16 | filename, target_lower_bound_string, target_upper_bound_string, digit_lower_bound_string = sys.argv 17 | digit_upper_bound_string = digit_lower_bound_string 18 | elif len(sys.argv) == 5: 19 | filename, target_lower_bound_string, target_upper_bound_string, digit_lower_bound_string, digit_upper_bound_string = sys.argv 20 | 21 | target_lower_bound = int(target_lower_bound_string) 22 | target_upper_bound = int(target_upper_bound_string) 23 | final_digit_lower_bound = int(digit_lower_bound_string) 24 | final_digit_upper_bound = int(digit_upper_bound_string) 25 | 26 | all_records = t.records.get_all() 27 | api_records = t.records.get_api_records() 28 | 29 | unsolved_count = 0 30 | unknown_count = 0 31 | 32 | for digits in xrange(final_digit_lower_bound, final_digit_upper_bound + 1): 33 | print "Processing #{}".format(digits) 34 | 35 | for target in xrange(target_lower_bound, target_upper_bound + 1): 36 | print target 37 | sub_records = api_records[digits] 38 | full_records =all_records[digits] 39 | if target not in sub_records: 40 | print "No API record for {}#{}".format(target, digits) 41 | t.cheater.solve(target, full_records[target], full_records, fail_fast=True) 42 | print 43 | unknown_count += 1 44 | elif sub_records[target] > full_records[target]: 45 | print "{}#{}: API: {}; GS: {}".format(target, digits, sub_records[target], full_records[target]) 46 | t.cheater.solve(target, full_records[target], full_records, fail_fast=True) 47 | print 48 | unknown_count += 1 49 | 50 | 51 | sorted_unsolved_problems = sorted(t.cheater.unsolved_problems.keys(), key=lambda x: (x[1], x[0])) 52 | unsolved_count = len(sorted_unsolved_problems) 53 | 54 | aggregated_unsolved_problems = {} 55 | for unsolved_problem in sorted_unsolved_problems: 56 | target = unsolved_problem[0] 57 | digits = unsolved_problem[1] 58 | count = t.cheater.unsolved_problems[unsolved_problem] 59 | 60 | if (digits, count) in aggregated_unsolved_problems: 61 | aggregated_unsolved_problems[(digits, count)].append(target) 62 | else: 63 | aggregated_unsolved_problems[(digits, count)] = [target] 64 | 65 | print "{} problems need to be improved. {} need to be solved. The rest can be easily deduced.".format(unknown_count, unsolved_count, unknown_count - unsolved_count) 66 | print "{} problem to be solved:".format(unsolved_count) 67 | for (digits, count) in sorted(aggregated_unsolved_problems.keys()): 68 | targets = aggregated_unsolved_problems[(digits, count)] 69 | targets_string = [str(target) for target in targets] 70 | 71 | print "[{}]#{} -d {}".format(','.join(targets_string), digits, count) 72 | 73 | 74 | -------------------------------------------------------------------------------- /range_cheater.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import tchisla as t 5 | 6 | skip_string = '' 7 | 8 | if len(sys.argv) == 2: 9 | filename, target_lower_bound_string = sys.argv 10 | target_upper_bound_string = target_lower_bound_string 11 | digit_lower_bound_string = '1' 12 | digit_upper_bound_string = '9' 13 | elif len(sys.argv) == 3: 14 | filename, target_lower_bound_string, target_upper_bound_string = sys.argv 15 | digit_lower_bound_string = '1' 16 | digit_upper_bound_string = '9' 17 | elif len(sys.argv) == 4: 18 | filename, target_lower_bound_string, target_upper_bound_string, digit_lower_bound_string = sys.argv 19 | digit_upper_bound_string = digit_lower_bound_string 20 | elif len(sys.argv) == 5: 21 | filename, target_lower_bound_string, target_upper_bound_string, digit_lower_bound_string, digit_upper_bound_string = sys.argv 22 | elif len(sys.argv) == 6: 23 | filename, target_lower_bound_string, target_upper_bound_string, digit_lower_bound_string, digit_upper_bound_string, skip_string = sys.argv 24 | 25 | target_lower_bound = int(target_lower_bound_string) 26 | target_upper_bound = int(target_upper_bound_string) 27 | final_digit_lower_bound = int(digit_lower_bound_string) 28 | final_digit_upper_bound = int(digit_upper_bound_string) 29 | skip = (skip_string == 'skip') 30 | 31 | all_records = t.records.get_all() 32 | api_records = t.records.get_api_records() 33 | 34 | for final_digit in xrange(final_digit_lower_bound, final_digit_upper_bound + 1): 35 | records = all_records[final_digit] 36 | for final_target in xrange(target_lower_bound, target_upper_bound + 1): 37 | if skip and final_target in api_records[final_digit] and api_records[final_digit][final_target] == all_records[final_digit][final_target]: 38 | continue 39 | 40 | if final_target not in records: 41 | print "No record for {}#{}".format(final_target, final_digit) 42 | else: 43 | final_digits_count = records[final_target] 44 | print "{}#{} ({})".format(final_target, final_digit, final_digits_count) 45 | solution = t.cheater.solve(final_target, final_digits_count, records, fail_fast=True) 46 | if solution is None: 47 | print solution 48 | else: 49 | print solution.encode('utf-8') 50 | if solution is not None: 51 | if final_target not in api_records[final_digit] or api_records[final_digit][final_target] > final_digits_count: 52 | t.records.submit(final_target, final_digit, final_digits_count, solution) 53 | 54 | print 55 | 56 | sorted_unsolved_problems = sorted(t.cheater.unsolved_problems.keys(), key=lambda x: (x[1], x[0])) 57 | 58 | aggregated_unsolved_problems = {} 59 | for unsolved_problem in sorted_unsolved_problems: 60 | target = unsolved_problem[0] 61 | digits = unsolved_problem[1] 62 | count = t.cheater.unsolved_problems[unsolved_problem] 63 | 64 | if (digits, count) in aggregated_unsolved_problems: 65 | aggregated_unsolved_problems[(digits, count)].append(target) 66 | else: 67 | aggregated_unsolved_problems[(digits, count)] = [target] 68 | 69 | print "Unsolved:" 70 | for (digits, count) in sorted(aggregated_unsolved_problems.keys()): 71 | targets = aggregated_unsolved_problems[(digits, count)] 72 | targets_string = [str(target) for target in targets] 73 | 74 | print "[{}]#{} -d {}".format(','.join(targets_string), digits, count) 75 | -------------------------------------------------------------------------------- /tchisla/records.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import requests 3 | import json 4 | import time 5 | import datetime 6 | 7 | DEFAULT_CACHE_LIMIT = 1000000000 8 | 9 | tmp_dir = 'tmp' 10 | cache_path = tmp_dir + "/records.json" 11 | 12 | solution_path = '../tchisla-solver/results/results.txt' 13 | 14 | def get_gs_records(): 15 | registry = {} 16 | for digits in xrange(1, 10): 17 | registry[digits] = {'digits': digits} 18 | 19 | gs_records_filename = 'gs_records.txt' 20 | 21 | with open(gs_records_filename, 'r') as f: 22 | for line in f: 23 | content = line.strip("\n") 24 | problem, digits_count = content.split(" ") 25 | target, digits = problem.split("#") 26 | target, digits, digits_count = int(target), int(digits), int(digits_count) 27 | registry[digits][target] = digits_count 28 | return registry 29 | 30 | def load(cache_limit=DEFAULT_CACHE_LIMIT): 31 | if not os.path.exists(tmp_dir): 32 | os.makedirs(tmp_dir) 33 | 34 | read_data = None 35 | if os.path.exists(cache_path): 36 | print "Loading records from " + cache_path 37 | with open(cache_path, 'r') as f: 38 | read_data = f.read() 39 | else: 40 | print "Loading records from API ..." 41 | resp = requests.get("http://www.euclidea.xyz/api/v1/game/numbers/solutions/records?&query={gte:1,lte:" + repr(cache_limit) + "}") 42 | read_data = resp.content 43 | print "Caching records ..." 44 | with open(cache_path, 'w') as f: 45 | f.write(read_data) 46 | 47 | all_records = json.loads(read_data)['records'] 48 | print "Loaded {0} records".format(len(all_records)) 49 | return all_records 50 | 51 | 52 | def inject_repeated_digits(registry): 53 | for digits in registry: 54 | for repeat_count in xrange(1, 10): 55 | number = digits * (10 ** repeat_count - 1) / 9 56 | if number not in registry[digits]: 57 | registry[digits][number] = repeat_count 58 | 59 | def get_api_records(cache_limit=DEFAULT_CACHE_LIMIT): 60 | return get_all(cache_limit, False) 61 | 62 | def get_all(cache_limit=DEFAULT_CACHE_LIMIT, merge_gs_records=True): 63 | registry = {} 64 | for digits in xrange(1, 10): 65 | registry[digits] = {'digits': digits} 66 | 67 | all_records = load(cache_limit) 68 | gs_records = get_gs_records() 69 | 70 | for record in all_records: 71 | digits = int(record['digits']) 72 | target = int(record['target']) 73 | digits_count = int(record['digits_count']) 74 | 75 | if digits >= 1 and digits <= 9: 76 | if merge_gs_records and target in gs_records[digits]: 77 | registry[digits][target] = min(digits_count, gs_records[digits][target]) 78 | else: 79 | registry[digits][target] = digits_count 80 | 81 | if merge_gs_records: 82 | for digits in xrange(1, 10): 83 | for target in gs_records[digits]: 84 | if target not in registry[digits]: 85 | registry[digits][target] = gs_records[digits][target] 86 | 87 | inject_repeated_digits(registry) 88 | return registry 89 | 90 | 91 | def get(digits, cache_limit=DEFAULT_CACHE_LIMIT): 92 | return get_all(cache_limit)[digits] 93 | 94 | def submit(target, digits, digits_count, solution): 95 | print u"Submitting {}#{}: {} {}".format(target, digits, digits_count, solution) 96 | data = { 97 | "records_timestamp" : str(int(time.time()) * 1000), 98 | "app" : "Numbers", 99 | "app_version" : "1.17", 100 | "app_family" : 0, 101 | "results" : [ 102 | { 103 | "solution" : solution, 104 | "digits_count" : digits_count, 105 | "digits" : digits, 106 | "target" : target, 107 | "date" : datetime.datetime.utcnow().isoformat() + "+00:00" 108 | } 109 | ], 110 | "os_version" : "9.3.4", 111 | "lang" : "zh", 112 | "orientation" : 1, 113 | "device" : "iPad2,1" 114 | } 115 | json_payload = json.dumps(data) 116 | submit_url = 'http://www.euclidea.xyz/api/v1/game/numbers/solutions' 117 | headers = {"Content-Type": "application/json", "Encoding": "utf-8"} 118 | resp = requests.post(submit_url, data = json_payload, headers = headers) 119 | resp_content = resp.content 120 | print json.loads(resp_content) 121 | 122 | def get_known_solutions(): 123 | print "Loading known solutions" 124 | solutions = {} 125 | with open(solution_path, 'r') as f: 126 | for line in f: 127 | content = line.decode('utf-8').strip("\n") 128 | target, digits, digits_count, solution = content.split(",") 129 | target, digits, digits_count = int(target), int(digits), int(digits_count) 130 | problem = (target, digits) 131 | if problem in solutions: 132 | if solutions[problem][0] > digits_count: 133 | solutions[problem] = (digits_count, solution) 134 | else: 135 | solutions[problem] = (digits_count, solution) 136 | return solutions 137 | -------------------------------------------------------------------------------- /tchisla/cheater.py: -------------------------------------------------------------------------------- 1 | import math 2 | from records import * 3 | 4 | reverse_factorial = {} 5 | for n in xrange(3, 20): 6 | reverse_factorial[math.factorial(n)] = n 7 | 8 | unsolved_problems = {} 9 | known_solutions = get_known_solutions() 10 | 11 | def solve(target, digits_count, registry, cache_limit=DEFAULT_CACHE_LIMIT, suppress_failure=False, fail_fast=True): 12 | digits = registry['digits'] 13 | problem = (target, digits) 14 | 15 | if problem in known_solutions: 16 | (known_digits_count, solution) = known_solutions[problem] 17 | if known_digits_count <= digits_count: 18 | print u"{} ({}) = {}".format( 19 | target, 20 | digits_count, 21 | solution 22 | ).encode('utf-8') 23 | return u"({})".format(solution) 24 | 25 | # rev_fac! == target 26 | if target in reverse_factorial: 27 | rev_fac = reverse_factorial[target] 28 | if rev_fac in registry and registry[rev_fac] <= digits_count: 29 | print "{} ({}) = {}!".format( 30 | target, 31 | digits_count, 32 | rev_fac 33 | ) 34 | inner = solve(rev_fac, registry[rev_fac], registry) 35 | if inner is None: 36 | if fail_fast: 37 | return None 38 | else: 39 | return u"({}!)".format(inner) 40 | 41 | for (operand_1, operand_1_count) in registry.iteritems(): 42 | if operand_1 == target: 43 | continue 44 | 45 | if operand_1 == 'digits': 46 | continue 47 | 48 | if operand_1_count >= digits_count: 49 | continue 50 | 51 | # operand_1 ** exponent == target 52 | if operand_1 > 1: 53 | for sqrt_count in xrange(0, 3): 54 | power = target ** (2 ** sqrt_count) 55 | exponent = int(math.log(power, operand_1) + 0.5) 56 | 57 | if operand_1 ** exponent == power: 58 | if exponent in registry and operand_1_count + registry[exponent] <= digits_count: 59 | print "{} ({}) = {}{}{} ^ {}".format( 60 | target, 61 | digits_count, 62 | "sqrt(" * sqrt_count, 63 | operand_1, 64 | ")" * sqrt_count, 65 | exponent, 66 | ) 67 | operand_1_solved = solve(operand_1, operand_1_count, registry) 68 | other_solved = solve(exponent, registry[exponent], registry) 69 | if operand_1_solved is None or other_solved is None: 70 | if fail_fast: 71 | return None 72 | else: 73 | return u"({}{} ^ {})".format(u'\u221a' * sqrt_count, operand_1_solved, other_solved) 74 | 75 | # operand_1 + or - plus_2 == target 76 | plus_2 = abs(target - operand_1) 77 | operator = '+' if (target > operand_1) else '-' 78 | if plus_2 in registry: 79 | if operand_1_count + registry[plus_2] <= digits_count: 80 | print "{} ({}) = {} {} {}".format( 81 | target, 82 | digits_count, 83 | operand_1, 84 | operator, 85 | plus_2 86 | ) 87 | operand_1_solved = solve(operand_1, operand_1_count, registry) 88 | other_solved = solve(plus_2, registry[plus_2], registry) 89 | if operand_1_solved is None or other_solved is None: 90 | if fail_fast: 91 | return None 92 | else: 93 | return u"({} {} {})".format(operand_1_solved, operator, other_solved) 94 | 95 | # operand_1 * times_2 == target 96 | if target % operand_1 == 0: 97 | times_2 = target / operand_1 98 | if times_2 in registry: 99 | if operand_1_count + registry[times_2] <= digits_count: 100 | print "{} ({}) = {} * {}".format( 101 | target, 102 | digits_count, 103 | operand_1, 104 | times_2 105 | ) 106 | operand_1_solved = solve(operand_1, operand_1_count, registry) 107 | other_solved = solve(times_2, registry[times_2], registry) 108 | if operand_1_solved is None or other_solved is None: 109 | if fail_fast: 110 | return None 111 | else: 112 | return u"({} * {})".format(operand_1_solved, other_solved) 113 | 114 | for (operand_1, operand_1_count) in registry.iteritems(): 115 | # numerator / operand_1 == target 116 | numerator = operand_1 * target 117 | if numerator <= cache_limit: 118 | if numerator in registry: 119 | if operand_1_count + registry[numerator] <= digits_count: 120 | print "{} ({}) = {} / {}".format( 121 | target, digits_count, 122 | numerator, 123 | operand_1, 124 | ) 125 | other_solved = solve(numerator, registry[numerator], registry) 126 | operand_1_solved = solve(operand_1, operand_1_count, registry) 127 | if operand_1_solved is None or other_solved is None: 128 | if fail_fast: 129 | return None 130 | else: 131 | return u"({} / {})".format(other_solved, operand_1_solved) 132 | 133 | if target > 1: 134 | # sqrt(square) == target 135 | square = target ** 2 136 | if square in registry and registry[square] <= digits_count: 137 | print "{} ({}) = sqrt({})".format( 138 | target, 139 | digits_count, 140 | square 141 | ) 142 | inner = solve(square, registry[square], registry) 143 | if inner is None: 144 | if fail_fast: 145 | return None 146 | else: 147 | return u'(\u221a{})'.format(inner) 148 | 149 | if str(target) == str(digits) * digits_count: 150 | return "{}".format(target) 151 | 152 | unsolved_problems[(target, digits)] = digits_count 153 | if not suppress_failure: 154 | print "{} ({}) = ?".format(target, digits_count) 155 | return None 156 | --------------------------------------------------------------------------------