diff --git a/minesweeper.py b/minesweeper.py index b307996..7b1d315 100644 --- a/minesweeper.py +++ b/minesweeper.py @@ -1,7 +1,8 @@ import collections import itertools import operator -from util import * +from .util import * +from functools import reduce set_ = frozenset @@ -176,9 +177,9 @@ def condense_supercells(rules): cell_rules_map = map_reduce(rules, lambda rule: [(cell, rule) for cell in rule.cells], set_) # for each 'list of rules appearing in', list of cells that share that ruleset (these cells # thus only ever appear together in the same rules) - rules_supercell_map = map_reduce(cell_rules_map.iteritems(), lambda (cell, rules): [(rules, cell)], set_) + rules_supercell_map = map_reduce(cell_rules_map.items(), lambda x: [(x[1], x[0])], set_) # for each original rule, list of 'supercells' appearing in that rule - rule_supercells_map = map_reduce(rules_supercell_map.iteritems(), lambda (rules, cell_): [(rule, cell_) for rule in rules], set_) + rule_supercells_map = map_reduce(rules_supercell_map.items(), lambda x: [(rule, x[1]) for rule in x[0]], set_) return ([rule.condensed(rule_supercells_map) for rule in rules], rules_supercell_map.values()) @@ -374,7 +375,7 @@ def combine(self, permu): """return a new permutation by combining this permutation with 'permu' the permutations must be compatible!""" - assert all(permu.mapping[k] == v for k, v in self.mapping.iteritems() if k in permu.mapping) + assert all(permu.mapping[k] == v for k, v in self.mapping.items() if k in permu.mapping) mapping = dict(self.mapping) mapping.update(permu.mapping) return Permutation(mapping) @@ -394,13 +395,13 @@ def multiplicity(self): e.g., N mines in a supercell of M cells has (M choose N) actual configurations """ - return product(choose(len(cell_), k) for cell_, k in self.mapping.iteritems()) + return product(choose(len(cell_), k) for cell_, k in self.mapping.items()) def _canonical(self): - return tuple(sorted(self.mapping.iteritems(), key=lambda (k, v): hash(k))) + return tuple(sorted(self.mapping.items(), key=lambda x: hash(x[0]))) def __repr__(self): - cell_counts = sorted([(sorted(list(cell)), count) for cell, count in self.mapping.iteritems()]) + cell_counts = sorted([(sorted(list(cell)), count) for cell, count in self.mapping.items()]) cell_frags = ['%s:%d' % (','.join(str(c) for c in cell), count) for cell, count in cell_counts] return '{%s}' % ' '.join(cell_frags) @@ -629,7 +630,7 @@ def rereduce(self): superseded_rules = set() decompositions = {} - for rule, permu_set in self.permu_map.iteritems(): + for rule, permu_set in self.permu_map.items(): decomp = permu_set.decompose() if len(decomp) > 1: superseded_rules.add(rule) @@ -709,7 +710,7 @@ def __init__(self, ruleset=None): # the current configuration-in-progress self.fixed = set() # subset of ruleset whose permutations are still 'open' - self.free = dict((rule, set(permu_set)) for rule, permu_set in ruleset.permu_map.iteritems()) + self.free = dict((rule, set(permu_set)) for rule, permu_set in ruleset.permu_map.items()) # helper function (closure) self.overlapping_rules = lambda rule: ruleset.cell_rules_map.overlapping_rules(rule) @@ -721,7 +722,7 @@ def clone(self): """clone this state""" state = EnumerationState() state.fixed = set(self.fixed) - state.free = dict((rule, set(permu_set)) for rule, permu_set in self.free.iteritems()) + state.free = dict((rule, set(permu_set)) for rule, permu_set in self.free.items()) state.overlapping_rules = self.overlapping_rules state.compatible_rule_index = self.compatible_rule_index return state @@ -729,7 +730,7 @@ def clone(self): def build_compatibility_index(self, rspm): """build the constraint index""" index = {} - for rule, permu_set in rspm.iteritems(): + for rule, permu_set in rspm.items(): for permu in permu_set: for rule_ov in self.overlapping_rules(rule): index[(permu, rule_ov)] = rspm[rule_ov].compatible(permu) @@ -845,7 +846,7 @@ def is_static(self): return len(self.subtallies) == 1 def __iter__(self): - return self.subtallies.iteritems() + return iter(self.subtallies.items()) def normalize(self): """normalize sub-tally totals into relative weights such that @@ -860,7 +861,7 @@ def collapse(self): all sub-tallies""" self.normalize() collapsed = map_reduce(self.subtallies.values(), lambda subtally: subtally.collapse(), sum) - for entry in collapsed.iteritems(): + for entry in collapsed.items(): yield entry def scale_weights(self, scalefunc): @@ -895,7 +896,7 @@ def for_other(num_uncharted_cells, mine_totals): """ metacell = UnchartedCell(num_uncharted_cells) - return FrontTally(dict((num_mines, FrontSubtally.mk(k, {metacell: num_mines})) for num_mines, k in mine_totals.iteritems())) + return FrontTally(dict((num_mines, FrontSubtally.mk(k, {metacell: num_mines})) for num_mines, k in mine_totals.items())) def __repr__(self): return str(dict(self.subtallies)) @@ -917,18 +918,18 @@ def add(self, config): """add a configuration to the tally""" mult = config.multiplicity() # weight by multiplicity self.total += mult - for cell_, n in config.mapping.iteritems(): + for cell_, n in config.mapping.items(): self.tally[cell_] += n * mult def finalize(self): """after all configurations have been summed, compute relative prevalence from totals""" - self.tally = dict((cell_, n / float(self.total)) for cell_, n in self.tally.iteritems()) + self.tally = dict((cell_, n / float(self.total)) for cell_, n in self.tally.items()) def collapse(self): """helper function for FrontTally.collapse(); emit all cell expected mine values weighted by this sub-tally's weight""" - for cell_, expected_mines in self.tally.iteritems(): + for cell_, expected_mines in self.tally.items(): yield (cell_, self.total * expected_mines) @staticmethod @@ -1064,7 +1065,8 @@ def new_front_total(): tallies = list(tallies) # we need guaranteed iteration order # iterate over the cartesian product of sub-tallies from each tally - for combination in (combo(e) for e in itertools.product(*tallies)): + for e in itertools.product(*tallies): + combination = combo(e) num_tallied_mines = sum(s.num_mines for s in combination) # number of mines lying in the 'other' cells num_free_mines = at_large_mines - num_tallied_mines diff --git a/util.py b/util.py index 0a2d955..4476e1e 100644 --- a/util.py +++ b/util.py @@ -1,10 +1,11 @@ import math import operator import collections +from functools import reduce def fact_div(a, b): """return a! / b!""" - return product(xrange(b + 1, a + 1)) if a >= b else 1. / fact_div(b, a) + return product(range(b + 1, a + 1)) if a >= b else 1. / fact_div(b, a) def choose(n, k): """return n choose k @@ -21,7 +22,7 @@ def peek(iterable): useful for extracting singletons, or when you're managing iteration yourself""" - return iter(iterable).next() + return next(iter(iterable)) def product(n): """return the product of a set of numbers @@ -62,7 +63,7 @@ def map_reduce(data, emitfunc=lambda rec: [(rec,)], reducefunc=lambda v: v): except ValueError: k, v = emission[0], None mapped[k].append(v) - return dict((k, reducefunc(v)) for k, v in mapped.iteritems()) + return dict((k, reducefunc(v)) for k, v in mapped.items()) class ImmutableMixin(object): """mixin for immutable, hashable objects""" diff --git a/web_demo/manage.py b/web_demo/manage.py index 5e78ea9..27014c5 100755 --- a/web_demo/manage.py +++ b/web_demo/manage.py @@ -1,11 +1,15 @@ #!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) - sys.exit(1) +import os +import sys -if __name__ == "__main__": - execute_manager(settings) +if __name__ == '__main__': + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web_demo.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/web_demo/minesweepr/static/script/minesweeper.js b/web_demo/minesweepr/static/script/minesweeper.js index f73aa97..76f0729 100644 --- a/web_demo/minesweepr/static/script/minesweeper.js +++ b/web_demo/minesweepr/static/script/minesweeper.js @@ -9,7 +9,7 @@ COUNT_FILL = ['blue', 'green', 'red', 'purple', 'brown', 'cyan', 'orange', 'blac MARGIN = 1; MINE_RADIUS = .5; FONT_SIZE = .5; -FONT_OFFSET = ($.browser.mozilla ? .15 : .07); +FONT_OFFSET = .07; FONT_SCALE_LONG = .8; HIGHLIGHT_CUR_CELL = 'rgba(0, 0, 0, 0)'; //'rgba(255, 180, 0, .2)'; HIGHLIGHT_NEIGHBOR = 'rgba(255, 220, 255, .2)'; diff --git a/web_demo/minesweepr/taskexec.py b/web_demo/minesweepr/taskexec.py index 040463a..d0d7100 100644 --- a/web_demo/minesweepr/taskexec.py +++ b/web_demo/minesweepr/taskexec.py @@ -26,7 +26,7 @@ def __init__(self, task, *args, **kwargs): self.kwargs = kwargs def start(self): - project_root = filter(lambda p: p, sys.path)[0] # sketchy + project_root = next(filter(lambda p: p, sys.path)) # sketchy self.p = Popen(['python', os.path.join(os.getcwd(), __file__)], cwd=project_root, stdin=PIPE, stdout=PIPE, stderr=PIPE) threading.Thread.start(self) @@ -34,7 +34,7 @@ def start(self): def run(self): payload = {'task': self.taskname, 'args': self.args, 'kwargs': self.kwargs} try: - out, err = self.p.communicate(ser.dumps(payload)) + out, err = self.p.communicate(ser.dumps(payload).encode()) self.result = ((False, err) if err else (True, ser.loads(out))) except: # various errors if process is terminated @@ -57,7 +57,7 @@ def resolve(self): if success: return result else: - raise Exception('error in task> ' + result) + raise Exception('error in task> ' + result.decode()) def _exec(payload): taskname = payload['task'] @@ -70,4 +70,4 @@ def _exec(payload): if __name__ == "__main__": sys.path.insert(0, os.getcwd()) # cwd set to django project root dir - print ser.dumps(_exec(ser.load(sys.stdin))) + print(ser.dumps(_exec(ser.load(sys.stdin)))) diff --git a/web_demo/minesweepr/templates/_player.html b/web_demo/minesweepr/templates/_player.html index b69ae53..74cb7af 100644 --- a/web_demo/minesweepr/templates/_player.html +++ b/web_demo/minesweepr/templates/_player.html @@ -5,13 +5,14 @@ {% block content_script %} {% endblock %} - - - - + +{% load static %} + + + @@ -213,7 +214,7 @@ - +
# mines left:
total accumulated risk:
solving…
solving…
solved in:
WIN diff --git a/web_demo/minesweepr/templates/_query.html b/web_demo/minesweepr/templates/_query.html deleted file mode 100644 index 93bb765..0000000 --- a/web_demo/minesweepr/templates/_query.html +++ /dev/null @@ -1,35 +0,0 @@ - - -{% block title %}{% endblock %} - -{% block content_script %} -{% endblock %} - - - - - - - - - -{% block main %} -{% endblock %} - -this feature is not yet written… - - - diff --git a/web_demo/minesweepr/templates/query.html b/web_demo/minesweepr/templates/query.html deleted file mode 100644 index 5c638e1..0000000 --- a/web_demo/minesweepr/templates/query.html +++ /dev/null @@ -1 +0,0 @@ -{% extends "_query.html" %} diff --git a/web_demo/minesweepr/urls.py b/web_demo/minesweepr/urls.py index 98657e9..bfb1af4 100644 --- a/web_demo/minesweepr/urls.py +++ b/web_demo/minesweepr/urls.py @@ -1,16 +1,9 @@ -from django.conf.urls.defaults import * -from django.conf import settings +from django.urls import path -dynurls = patterns('minesweepr.views', - (r'^api/minesweeper_solve/$', 'api_solve'), -) +from . import views -staticurls = patterns('minesweepr.views', - (r'^player/$', 'template_static'), - (r'^query/$', 'template_static'), -) - -urlpatterns = patterns('', - ('^%s' % settings.BASE_URL, include(dynurls)), - ('^%s' % settings.BASE_STATIC_URL, include(staticurls)), -) +urlpatterns = [ + path('player/', views.template_static), + path('query/', views.template_static), + path('api/minesweeper_solve/', views.api_solve, name='minesweepr.views.api_solve'), +] diff --git a/web_demo/minesweepr/views.py b/web_demo/minesweepr/views.py index 0845809..cea5981 100644 --- a/web_demo/minesweepr/views.py +++ b/web_demo/minesweepr/views.py @@ -1,18 +1,17 @@ -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound +from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt -from django.template import RequestContext -from django.shortcuts import render_to_response +from django.shortcuts import render from django.conf import settings import json import logging import time -from tasks import minesweeper_solve -from taskexec import exec_capped, ExecTimeOut +from .tasks import minesweeper_solve +from .taskexec import exec_capped, ExecTimeOut import itertools @csrf_exempt def api_solve(request): - payload = json.loads(request.raw_post_data) + payload = json.loads(request.body) logging.debug('>>' + str(payload)) start = time.time() @@ -23,7 +22,7 @@ def api_solve(request): log_result(payload, result, time.time() - start) logging.debug('<<' + str(result)) - return HttpResponse(json.dumps(result), 'text/json') + return JsonResponse(result) def log_result(payload, result, rtt): num_rules = len(payload['rules']) @@ -42,4 +41,4 @@ def template_static(request): url = request.path[1:-1] assert url.startswith(settings.BASE_STATIC_URL) template = url[len(settings.BASE_STATIC_URL):] + '.html' - return render_to_response(template, {}, context_instance=RequestContext(request)) + return render(request, template, {}) diff --git a/web_demo/settings.py b/web_demo/settings.py deleted file mode 100644 index f8a7397..0000000 --- a/web_demo/settings.py +++ /dev/null @@ -1,117 +0,0 @@ -import django # for verison sniffing - -# Django settings for minesweeper project. - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - # ('Your Name', 'your_email@domain.com'), -) - -MANAGERS = ADMINS - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': '', # Or path to database file if using sqlite3. - 'USER': '', # Not used with sqlite3. - 'PASSWORD': '', # Not used with sqlite3. - 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3. - } -} - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = None - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash if there is a path component (optional in other cases). -# Examples: "http://media.lawrence.com", "http://example.com/media/" -MEDIA_URL = '' - -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/media/' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.core.context_processors.static' if django.VERSION >= (1, 3) else 'staticfiles.context_processors.static', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -) - -ROOT_URLCONF = 'urls' - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', - 'django.contrib.staticfiles' if django.VERSION >= (1, 3) else 'staticfiles', - 'minesweepr', -) - -# root url for project -BASE_URL = '' -# root url for pages that will be pre-rendered to static content for deployment -# i.e., they only exist as templates for reverse-url lookup -BASE_STATIC_URL = '' - -STATIC_URL = '/static/' -STATIC_ROOT = None - -CPU_QUOTA = 5. #s - -try: - from localsettings import * -except ImportError: - pass diff --git a/web_demo/urls.py b/web_demo/urls.py deleted file mode 100644 index 34fadfc..0000000 --- a/web_demo/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.conf.urls.defaults import * -from django.conf import settings - -# Uncomment the next two lines to enable the admin: -# from django.contrib import admin -# admin.autodiscover() - -urlpatterns = patterns('', - ('^', include('minesweepr.urls')), -) - -urlpatterns += patterns('', - # Uncomment the admin/doc line below to enable admin documentation: - # (r'^admin/doc/', include('django.contrib.admindocs.urls')), - - # Uncomment the next line to enable the admin: - # (r'^admin/', include(admin.site.urls)), -) diff --git a/web_demo/__init__.py b/web_demo/web_demo/__init__.py similarity index 100% rename from web_demo/__init__.py rename to web_demo/web_demo/__init__.py diff --git a/web_demo/web_demo/settings.py b/web_demo/web_demo/settings.py new file mode 100644 index 0000000..9195059 --- /dev/null +++ b/web_demo/web_demo/settings.py @@ -0,0 +1,69 @@ +""" +Django settings for web_demo project. + +Generated by 'django-admin startproject' using Django 2.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/2.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.1/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'a1*p5#lybi*a(juhs0(f3hnbzv(w1497+@#8utkngp^58amb16' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.contenttypes', + 'django.contrib.staticfiles', +] + +ROOT_URLCONF = 'web_demo.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': ['minesweepr/templates'], + }, +] + +WSGI_APPLICATION = 'web_demo.wsgi.application' + +# Internationalization +# https://docs.djangoproject.com/en/2.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.1/howto/static-files/ + +STATIC_URL = '/static/' + +BASE_STATIC_URL = '' +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'minesweepr', 'static'), +] +CPU_QUOTA = 5. # s diff --git a/web_demo/web_demo/urls.py b/web_demo/web_demo/urls.py new file mode 100644 index 0000000..3a67fe6 --- /dev/null +++ b/web_demo/web_demo/urls.py @@ -0,0 +1,20 @@ +"""web_demo URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import include, path + +urlpatterns = [ + path('', include('minesweepr.urls')), +] diff --git a/web_demo/web_demo/wsgi.py b/web_demo/web_demo/wsgi.py new file mode 100644 index 0000000..6627ad3 --- /dev/null +++ b/web_demo/web_demo/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for web_demo project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web_demo.settings') + +application = get_wsgi_application()