aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnthony Wang2021-02-08 15:49:40 -0600
committerAnthony Wang2021-02-08 15:49:40 -0600
commitfaecf57006dea5ae609d9a1ee574c4fff47533c9 (patch)
tree57a304701fa0532e815216ce9218fedd7aaa513f
parent8641824fe6c5bace3fef7cf683874eef331b3acc (diff)
Add visualizers
-rwxr-xr-x[-rw-r--r--]ai/1.py1
-rwxr-xr-xai/1vi.py87
-rwxr-xr-x[-rw-r--r--]ai/2.py1
-rwxr-xr-xai/2vi.py837
-rwxr-xr-x[-rw-r--r--]ai/3.py1
-rwxr-xr-xai/3vi.py870
6 files changed, 1797 insertions, 0 deletions
diff --git a/ai/1.py b/ai/1.py
index df4446b..fa00e1b 100644..100755
--- a/ai/1.py
+++ b/ai/1.py
@@ -1,3 +1,4 @@
+#!/usr/bin/python
# Bet Starter File
# NOTE: You can run this file locally to test if your program is working.
diff --git a/ai/1vi.py b/ai/1vi.py
new file mode 100755
index 0000000..8071bd6
--- /dev/null
+++ b/ai/1vi.py
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+import json
+import copy
+
+fileName = str(input("Please type the exact file name of the match you want to visualize. Ensure that your match data and this file are in the same directory:\n"))
+
+with open (fileName) as f:
+ data = json.load(f)
+
+table = [['0' for i in range(7)] for j in range(13)]
+
+numRounds = len(data['gamedata'])
+print("There are {} games in this match.".format(numRounds))
+index = int(input("Enter which game you want to view from 1 to {}:\n".format(numRounds)))
+assert index > 0 and index < numRounds+1,"Did not enter valid game number."
+seatNum = data['seat']
+
+for item in data['gamedata'][index-1]['hist']:
+ # each item is a round corresponding to one of the 13 cards from the deck
+ table[item['time']-1][0] = item['card']
+
+ for i in range(3):
+ table[item['time']-1][i+1] = item['moves'][i]['using']
+ if item['moves'][seatNum-1]['got'] == [] or item['moves'][seatNum-1]['got']!=item['moves'][seatNum-1]['using']:
+ table[item['time']-1][3+seatNum] = item['moves'][seatNum-1]['error']
+
+output = "\nYou are P1. Your opponents are P2 and P3.\n\n"
+totals = {}
+totals["P1"] = 0
+totals["P2"] = 0
+totals["P3"] = 0
+
+for row in table:
+ scores = {}
+ scores["P1"] = row[seatNum]
+ if seatNum == 1:
+ scores["P2"] = row[2]
+ scores["P3"] = row[3]
+ elif seatNum == 2:
+ scores["P2"] = row[1]
+ scores["P3"] = row[3]
+ else:
+ scores["P2"] = row[1]
+ scores["P3"] = row[2]
+
+
+ vals = copy.deepcopy(row)
+ vals = [vals[1],vals[2],vals[3]]
+ scoresInc = sorted(vals)
+ currRound = str(table.index(row)+1)
+ if row[3+seatNum] == '0':
+ if scoresInc[2] != scoresInc[1]:
+ player = max(scores, key=scores.get)
+ added = "Round {}: {} wins {} points\n".format(currRound,player,str(row[0]))
+ totals[player] += row[0]
+ elif len(set(scoresInc))==2:
+ a,b = [player for player, score in scores.items() if score == max(scores.values())]
+ added = "Round {}: There was a collision between {} and {}\n".format(currRound,a,b)
+ elif len(set(scoresInc))==1:
+ added = "Round {}: There was a collision between P1, P2, and P3\n".format(currRound)
+ else:
+ added = "Round {}: Your code threw this error: {}.\n".format(currRound, row[3+seatNum])
+ added += "Round {}: As a result, your lowest card was played.\n".format(currRound)
+ if scoresInc[2] != scoresInc[1]:
+ player = max(scores, key=scores.get)
+ added += "Round {}: {} wins {} points\n".format(currRound,player,str(row[0]))
+ totals[player] += row[0]
+ elif len(set(scoresInc))==2:
+ a,b = [player for player, score in scores.items() if score == max(scores.values())]
+ added += "Round {}: There was a collision between {} and {}\n".format(currRound,a,b)
+ elif len(set(scoresInc))==1:
+ added += "Round {}: There was a collision between P1, P2, and P3\n".format(currRound)
+ output += added
+
+sortedTotals = {player: total for player, total in sorted(totals.items(), key=lambda item: item[1])}
+
+print(output)
+
+print("Player {} came in 1st place with a score of {}".format(list(sortedTotals)[2],list(sortedTotals.values())[2]))
+print("Player {} came in 2nd place with a score of {}".format(list(sortedTotals)[1],list(sortedTotals.values())[1]))
+print("Player {} came in 3rd place with a score of {}".format(list(sortedTotals)[0],list(sortedTotals.values())[0]))
+yourScore = totals['P1']
+yourRank = 3-list(sortedTotals).index('P1')
+rankPoints=[5,2,1]
+yourRankPoints = rankPoints[2-list(sortedTotals).index('P1')]
+bonusPoints = round(0.01*yourScore,3)
+print("As a result, you ranked in place {} and received {} + {} = {} points for this game.".format(yourRank,yourRankPoints,bonusPoints,yourRankPoints+bonusPoints)) \ No newline at end of file
diff --git a/ai/2.py b/ai/2.py
index 5c3d9d2..e53e7ba 100644..100755
--- a/ai/2.py
+++ b/ai/2.py
@@ -1,3 +1,4 @@
+#!/usr/bin/python
# Scotty Dog Starter File
# NOTE: You can run this file locally to test if your program is working.
diff --git a/ai/2vi.py b/ai/2vi.py
new file mode 100755
index 0000000..53b39c2
--- /dev/null
+++ b/ai/2vi.py
@@ -0,0 +1,837 @@
+#!/usr/bin/python
+import json,time
+
+# Problem Specific visualizer code starts at line 748
+################################################################################
+# cmu_112_graphics.py
+# version 0.8.6
+
+# Pre-release for CMU 15-112-f20
+
+# Require Python 3.6 or later
+import sys
+if ((sys.version_info[0] != 3) or (sys.version_info[1] < 6)):
+ raise Exception('cmu_112_graphics.py requires Python version 3.6 or later.')
+
+# Track version and file update timestamp
+import datetime
+MAJOR_VERSION = 0
+MINOR_VERSION = 8.6 # version 0.8.6
+LAST_UPDATED = datetime.date(year=2020, month=2, day=24)
+
+# Pending changes:
+# * Fix Windows-only bug: Position popup dialog box over app window (already works fine on Macs)
+# * Add documentation
+# * integrate sounds (probably from pyGame)
+# * Improved methodIsOverridden to TopLevelApp and ModalApp
+# * Save to animated gif and/or mp4 (with audio capture?)
+
+# Deferred changes:
+# * replace/augment tkinter canvas with PIL/Pillow imageDraw (perhaps with our own fn names)
+# * use snake_case and CapWords
+
+# Chages in v0.8.6
+# * f20
+
+# Chages in v0.8.5
+# * Support loadImage from Modes
+
+# Chages in v0.8.3 + v0.8.4
+# * Use default empty Mode if none is provided
+# * Add KeyRelease event binding
+# * Drop user32.SetProcessDPIAware (caused window to be really tiny on some Windows machines)
+
+# Changes in v0.8.1 + v0.8.2
+# * print version number and last-updated date on load
+# * restrict modifiers to just control key (was confusing with NumLock, etc)
+# * replace hasModifiers with 'control-' prefix, as in 'control-A'
+# * replace app._paused with app.paused, etc (use app._ for private variables)
+# * use improved ImageGrabber import for linux
+
+# Changes in v0.8.0
+# * suppress more modifier keys (Super_L, Super_R, ...)
+# * raise exception on event.keysym or event.char + works with key = 'Enter'
+# * remove tryToInstall
+
+# Changes in v0.7.4
+# * renamed drawAll back to redrawAll :-)
+
+# Changes in v0.7.3
+# * Ignore mousepress-drag-release and defer configure events for drags in titlebar
+# * Extend deferredRedrawAll to 100ms with replace=True and do not draw while deferred
+# (together these hopefully fix Windows-only bug: file dialog makes window not moveable)
+# * changed sizeChanged to not take event (use app.width and app.height)
+
+# Changes in v0.7.2
+# * Singleton App._theRoot instance (hopefully fixes all those pesky Tkinter errors-on-exit)
+# * Use user32.SetProcessDPIAware to get resolution of screen grabs right on Windows-only (fine on Macs)
+# * Replaces showGraphics() with runApp(...), which is a veneer for App(...) [more intuitive for pre-OOP part of course]
+# * Fixes/updates images:
+# * disallows loading images in redrawAll (raises exception)
+# * eliminates cache from loadImage
+# * eliminates app.getTkinterImage, so user now directly calls ImageTk.PhotoImage(image))
+# * also create_image allows magic pilImage=image instead of image=ImageTk.PhotoImage(app.image)
+
+# Changes in v0.7.1
+# * Added keyboard shortcut:
+# * cmd/ctrl/alt-x: hard exit (uses os._exit() to exit shell without tkinter error messages)
+# * Fixed bug: shortcut keys stopped working after an MVC violation (or other exception)
+# * In app.saveSnapshot(), add .png to path if missing
+# * Added: Print scripts to copy-paste into shell to install missing modules (more automated approaches proved too brittle)
+
+# Changes in v0.7
+# * Added some image handling (requires PIL (retained) and pyscreenshot (later removed):
+# * app.loadImage() # loads PIL/Pillow image from file, with file dialog, or from URL (http or https)
+# * app.scaleImage() # scales a PIL/Pillow image
+# * app.getTkinterImage() # converts PIL/Pillow image to Tkinter PhotoImage for use in create_image(...)
+# * app.getSnapshot() # get a snapshot of the canvas as a PIL/Pillow image
+# * app.saveSnapshot() # get and save a snapshot
+# * Added app._paused, app.togglePaused(), and paused highlighting (red outline around canvas when paused)
+# * Added keyboard shortcuts:
+# * cmd/ctrl/alt-s: save a snapshot
+# * cmd/ctrl/alt-p: pause/unpause
+# * cmd/ctrl/alt-q: quit
+
+# Changes in v0.6:
+# * Added fnPrefix option to TopLevelApp (so multiple TopLevelApp's can be in one file)
+# * Added showGraphics(drawFn) (for graphics-only drawings before we introduce animations)
+
+# Changes in v0.5:
+# * Added:
+# * app.winx and app.winy (and add winx,winy parameters to app.__init__, and sets these on configure events)
+# * app.setSize(width, height)
+# * app.setPosition(x, y)
+# * app.quit()
+# * app.showMessage(message)
+# * app.getUserInput(prompt)
+# * App.lastUpdated (instance of datetime.date)
+# * Show popup dialog box on all exceptions (not just for MVC violations)
+# * Draw (in canvas) "Exception! App Stopped! (See console for details)" for any exception
+# * Replace callUserMethod() with more-general @_safeMethod decorator (also handles exceptions outside user methods)
+# * Only include lines from user's code (and not our framework nor tkinter) in stack traces
+# * Require Python version (3.6 or greater)
+
+# Changes in v0.4:
+# * Added __setattr__ to enforce Type 1A MVC Violations (setting app.x in redrawAll) with better stack trace
+# * Added app._deferredRedrawAll() (avoids resizing drawing/crashing bug on some platforms)
+# * Added deferredMethodCall() and app._afterIdMap to generalize afterId handling
+# * Use (_ is None) instead of (_ == None)
+
+# Changes in v0.3:
+# * Fixed "event not defined" bug in sizeChanged handlers.
+# * draw "MVC Violation" on Type 2 violation (calling draw methods outside redrawAll)
+
+# Changes in v0.2:
+# * Handles another MVC violation (now detects drawing on canvas outside of redrawAll)
+# * App stops running when an exception occurs (in user code) (stops cascading errors)
+
+# Changes in v0.1:
+# * OOPy + supports inheritance + supports multiple apps in one file + etc
+# * uses import instead of copy-paste-edit starter code + no "do not edit code below here!"
+# * no longer uses Struct (which was non-Pythonic and a confusing way to sort-of use OOP)
+# * Includes an early version of MVC violation handling (detects model changes in redrawAll)
+# * added events:
+# * appStarted (no init-vs-__init__ confusion)
+# * appStopped (for cleanup)
+# * keyReleased (well, sort of works) + mouseReleased
+# * mouseMoved + mouseDragged
+# * sizeChanged (when resizing window)
+# * improved key names (just use event.key instead of event.char and/or event.keysym + use names for 'Enter', 'Escape', ...)
+# * improved function names (renamed redrawAll to drawAll)
+# * improved (if not perfect) exiting without that irksome Tkinter error/bug
+# * app has a title in the titlebar (also shows window's dimensions)
+# * supports Modes and ModalApp (see ModalApp and Mode, and also see TestModalApp example)
+# * supports TopLevelApp (using top-level functions instead of subclasses and methods)
+# * supports version checking with App.majorVersion, App.minorVersion, and App.version
+# * logs drawing calls to support autograding views (still must write that autograder, but this is a very helpful first step)
+
+from tkinter import *
+from tkinter import messagebox, simpledialog, filedialog
+import inspect, copy, traceback
+import sys, os
+from io import BytesIO
+
+def failedImport(importName, installName=None): pass
+
+try: from PIL import Image, ImageTk
+except ModuleNotFoundError: failedImport('PIL', 'pillow')
+
+if sys.platform.startswith('linux'):
+ try: import pyscreenshot as ImageGrabber
+ except ModuleNotFoundError: failedImport('pyscreenshot')
+else:
+ try: from PIL import ImageGrab as ImageGrabber
+ except ModuleNotFoundError: pass # Our PIL warning is already printed above
+
+try: import requests
+except ModuleNotFoundError: failedImport('requests')
+
+def getHash(obj):
+ # This is used to detect MVC violations in redrawAll
+ # @TODO: Make this more robust and efficient
+ try:
+ return getHash(obj.__dict__)
+ except:
+ if (isinstance(obj, list)): return getHash(tuple([getHash(v) for v in obj]))
+ elif (isinstance(obj, set)): return getHash(sorted(obj))
+ elif (isinstance(obj, dict)): return getHash(tuple([obj[key] for key in sorted(obj)]))
+ else:
+ try: return hash(obj)
+ except: return getHash(repr(obj))
+
+class WrappedCanvas(Canvas):
+ # Enforces MVC: no drawing outside calls to redrawAll
+ # Logs draw calls (for autograder) in canvas.loggedDrawingCalls
+ def __init__(wrappedCanvas, app):
+ wrappedCanvas.loggedDrawingCalls = [ ]
+ wrappedCanvas.logDrawingCalls = True
+ wrappedCanvas.inRedrawAll = False
+ wrappedCanvas.app = app
+ super().__init__(app._root, width=app.width, height=app.height)
+
+ def log(self, methodName, args, kwargs):
+ if (not self.inRedrawAll):
+ self.app._mvcViolation('you may not use the canvas (the view) outside of redrawAll')
+ if (self.logDrawingCalls):
+ self.loggedDrawingCalls.append((methodName, args, kwargs))
+
+ def create_arc(self, *args, **kwargs): self.log('create_arc', args, kwargs); return super().create_arc(*args, **kwargs)
+ def create_bitmap(self, *args, **kwargs): self.log('create_bitmap', args, kwargs); return super().create_bitmap(*args, **kwargs)
+ def create_line(self, *args, **kwargs): self.log('create_line', args, kwargs); return super().create_line(*args, **kwargs)
+ def create_oval(self, *args, **kwargs): self.log('create_oval', args, kwargs); return super().create_oval(*args, **kwargs)
+ def create_polygon(self, *args, **kwargs): self.log('create_polygon', args, kwargs); return super().create_polygon(*args, **kwargs)
+ def create_rectangle(self, *args, **kwargs): self.log('create_rectangle', args, kwargs); return super().create_rectangle(*args, **kwargs)
+ def create_text(self, *args, **kwargs): self.log('create_text', args, kwargs); return super().create_text(*args, **kwargs)
+ def create_window(self, *args, **kwargs): self.log('create_window', args, kwargs); return super().create_window(*args, **kwargs)
+
+ def create_image(self, *args, **kwargs):
+ self.log('create_image', args, kwargs);
+ usesImage = 'image' in kwargs
+ usesPilImage = 'pilImage' in kwargs
+ if ((not usesImage) and (not usesPilImage)):
+ raise Exception('create_image requires an image to draw')
+ elif (usesImage and usesPilImage):
+ raise Exception('create_image cannot use both an image and a pilImage')
+ elif (usesPilImage):
+ pilImage = kwargs['pilImage']
+ del kwargs['pilImage']
+ if (not isinstance(pilImage, Image.Image)):
+ raise Exception('create_image: pilImage value is not an instance of a PIL/Pillow image')
+ image = ImageTk.PhotoImage(pilImage)
+ else:
+ image = kwargs['image']
+ if (isinstance(image, Image.Image)):
+ raise Exception('create_image: image must not be an instance of a PIL/Pillow image\n' +
+ 'You perhaps meant to convert from PIL to Tkinter, like so:\n' +
+ ' canvas.create_image(x, y, image=ImageTk.PhotoImage(image))')
+ kwargs['image'] = image
+ return super().create_image(*args, **kwargs)
+
+class App(object):
+ majorVersion = MAJOR_VERSION
+ minorVersion = MINOR_VERSION
+ version = f'{majorVersion}.{minorVersion}'
+ lastUpdated = LAST_UPDATED
+ _theRoot = None # singleton Tkinter root object
+
+ ####################################
+ # User Methods:
+ ####################################
+ def redrawAll(app, canvas): pass # draw (view) the model in the canvas
+ def appStarted(app): pass # initialize the model (app.xyz)
+ def appStopped(app): pass # cleanup after app is done running
+ def keyPressed(app, event): pass # use event.key
+ def keyReleased(app, event): pass # use event.key
+ def mousePressed(app, event): pass # use event.x and event.y
+ def mouseReleased(app, event): pass # use event.x and event.y
+ def mouseMoved(app, event): pass # use event.x and event.y
+ def mouseDragged(app, event): pass # use event.x and event.y
+ def timerFired(app): pass # respond to timer events
+ def sizeChanged(app): pass # respond to window size changes
+
+ ####################################
+ # Implementation:
+ ####################################
+
+ def __init__(app, width=300, height=300, x=0, y=0, title=None, autorun=True, mvcCheck=True, logDrawingCalls=True):
+ app.winx, app.winy, app.width, app.height = x, y, width, height
+ app.timerDelay = 100 # milliseconds
+ app.mouseMovedDelay = 50 # ditto
+ app._title = title
+ app._mvcCheck = mvcCheck
+ app._logDrawingCalls = logDrawingCalls
+ app._running = app._paused = False
+ app._mousePressedOutsideWindow = False
+ if autorun: app.run()
+
+ def setSize(app, width, height):
+ app._root.geometry(f'{width}x{height}')
+
+ def setPosition(app, x, y):
+ app._root.geometry(f'+{x}+{y}')
+
+ def showMessage(app, message):
+ messagebox.showinfo('showMessage', message, parent=app._root)
+
+ def getUserInput(app, prompt):
+ return simpledialog.askstring('getUserInput', prompt)
+
+ def loadImage(app, path=None):
+ if (app._canvas.inRedrawAll):
+ raise Exception('Cannot call loadImage in redrawAll')
+ if (path is None):
+ path = filedialog.askopenfilename(initialdir=os.getcwd(), title='Select file: ',filetypes = (('Image files','*.png *.gif *.jpg'),('all files','*.*')))
+ if (not path): return None
+ if (path.startswith('http')):
+ response = requests.request('GET', path) # path is a URL!
+ image = Image.open(BytesIO(response.content))
+ else:
+ image = Image.open(path)
+ return image
+
+ def scaleImage(app, image, scale, antialias=False):
+ # antialiasing is higher-quality but slower
+ resample = Image.ANTIALIAS if antialias else Image.NEAREST
+ return image.resize((round(image.width*scale), round(image.height*scale)), resample=resample)
+
+ def getSnapshot(app):
+ app._showRootWindow()
+ x0 = app._root.winfo_rootx() + app._canvas.winfo_x()
+ y0 = app._root.winfo_rooty() + app._canvas.winfo_y()
+ result = ImageGrabber.grab((x0,y0,x0+app.width,y0+app.height))
+ return result
+
+ def saveSnapshot(app):
+ path = filedialog.asksaveasfilename(initialdir=os.getcwd(), title='Select file: ',filetypes = (('png files','*.png'),('all files','*.*')))
+ if (path):
+ # defer call to let filedialog close (and not grab those pixels)
+ if (not path.endswith('.png')): path += '.png'
+ app._deferredMethodCall(afterId='saveSnapshot', afterDelay=0, afterFn=lambda:app.getSnapshot().save(path))
+
+ def _togglePaused(app):
+ app._paused = not app._paused
+
+ def quit(app):
+ app._running = False
+ app._root.quit() # break out of root.mainloop() without closing window!
+
+ def __setattr__(app, attr, val):
+ d = app.__dict__
+ d[attr] = val
+ canvas = d.get('_canvas', None)
+ if (d.get('running', False) and
+ d.get('mvcCheck', False) and
+ (canvas is not None) and
+ canvas.inRedrawAll):
+ app._mvcViolation(f'you may not change app.{attr} in the model while in redrawAll (the view)')
+
+ def _printUserTraceback(app, exception, tb):
+ stack = traceback.extract_tb(tb)
+ lines = traceback.format_list(stack)
+ inRedrawAllWrapper = False
+ printLines = [ ]
+ for line in lines:
+ if (('"cmu_112_graphics.py"' not in line) and
+ ('/cmu_112_graphics.py' not in line) and
+ ('\\cmu_112_graphics.py' not in line) and
+ ('/tkinter/' not in line) and
+ ('\\tkinter\\' not in line)):
+ printLines.append(line)
+ if ('redrawAllWrapper' in line):
+ inRedrawAllWrapper = True
+ if (len(printLines) == 0):
+ # No user code in trace, so we have to use all the code (bummer),
+ # but not if we are in a redrawAllWrapper...
+ if inRedrawAllWrapper:
+ printLines = [' No traceback available. Error occurred in redrawAll.\n']
+ else:
+ printLines = lines
+ print('Traceback (most recent call last):')
+ for line in printLines: print(line, end='')
+ print(f'Exception: {exception}')
+
+ def _safeMethod(appMethod):
+ def m(*args, **kwargs):
+ app = args[0]
+ try:
+ return appMethod(*args, **kwargs)
+ except Exception as e:
+ app._running = False
+ app._printUserTraceback(e, sys.exc_info()[2])
+ if ('_canvas' in app.__dict__):
+ app._canvas.inRedrawAll = True # not really, but stops recursive MVC Violations!
+ app._canvas.create_rectangle(0, 0, app.width, app.height, fill=None, width=10, outline='red')
+ app._canvas.create_rectangle(10, app.height-50, app.width-10, app.height-10,
+ fill='white', outline='red', width=4)
+ app._canvas.create_text(app.width/2, app.height-40, text=f'Exception! App Stopped!', fill='red', font='Arial 12 bold')
+ app._canvas.create_text(app.width/2, app.height-20, text=f'See console for details', fill='red', font='Arial 12 bold')
+ app._canvas.update()
+ app.showMessage(f'Exception: {e}\nClick ok then see console for details.')
+ return m
+
+ def _methodIsOverridden(app, methodName):
+ return (getattr(type(app), methodName) is not getattr(App, methodName))
+
+ def _mvcViolation(app, errMsg):
+ app._running = False
+ raise Exception('MVC Violation: ' + errMsg)
+
+ @_safeMethod
+ def _redrawAllWrapper(app):
+ if (not app._running): return
+ if ('deferredRedrawAll' in app._afterIdMap): return # wait for pending call
+ app._canvas.inRedrawAll = True
+ app._canvas.delete(ALL)
+ width,outline = (10,'red') if app._paused else (0,'white')
+ app._canvas.create_rectangle(0, 0, app.width, app.height, fill='white', width=width, outline=outline)
+ app._canvas.loggedDrawingCalls = [ ]
+ app._canvas.logDrawingCalls = app._logDrawingCalls
+ hash1 = getHash(app) if app._mvcCheck else None
+ try:
+ app.redrawAll(app._canvas)
+ hash2 = getHash(app) if app._mvcCheck else None
+ if (hash1 != hash2):
+ app._mvcViolation('you may not change the app state (the model) in redrawAll (the view)')
+ finally:
+ app._canvas.inRedrawAll = False
+ app._canvas.update()
+
+ def _deferredMethodCall(app, afterId, afterDelay, afterFn, replace=False):
+ def afterFnWrapper():
+ app._afterIdMap.pop(afterId, None)
+ afterFn()
+ id = app._afterIdMap.get(afterId, None)
+ if ((id is None) or replace):
+ if id: app._root.after_cancel(id)
+ app._afterIdMap[afterId] = app._root.after(afterDelay, afterFnWrapper)
+
+ def _deferredRedrawAll(app):
+ app._deferredMethodCall(afterId='deferredRedrawAll', afterDelay=100, afterFn=app._redrawAllWrapper, replace=True)
+
+ @_safeMethod
+ def _appStartedWrapper(app):
+ app.appStarted()
+ app._redrawAllWrapper()
+
+ _keyNameMap = { '\t':'Tab', '\n':'Enter', '\r':'Enter', '\b':'Backspace',
+ chr(127):'Delete', chr(27):'Escape', ' ':'Space' }
+
+ @staticmethod
+ def _useEventKey(attr):
+ raise Exception(f'Use event.key instead of event.{attr}')
+
+ @staticmethod
+ def _getEventKeyInfo(event, keysym, char):
+ key = c = char
+ hasControlKey = (event.state & 0x4 != 0)
+ if ((c in [None, '']) or (len(c) > 1) or (ord(c) > 255)):
+ key = keysym
+ if (key.endswith('_L') or
+ key.endswith('_R') or
+ key.endswith('_Lock')):
+ key = 'Modifier_Key'
+ elif (c in App._keyNameMap):
+ key = App._keyNameMap[c]
+ elif ((len(c) == 1) and (1 <= ord(c) <= 26)):
+ key = chr(ord('a')-1 + ord(c))
+ hasControlKey = True
+ if hasControlKey and (len(key) == 1):
+ # don't add control- prefix to Enter, Tab, Escape, ...
+ key = 'control-' + key
+ return key
+
+ class KeyEventWrapper(Event):
+ def __init__(self, event):
+ keysym, char = event.keysym, event.char
+ del event.keysym
+ del event.char
+ for key in event.__dict__:
+ if (not key.startswith('__')):
+ self.__dict__[key] = event.__dict__[key]
+ self.key = App._getEventKeyInfo(event, keysym, char)
+ keysym = property(lambda *args: App._useEventKey('keysym'),
+ lambda *args: App._useEventKey('keysym'))
+ char = property(lambda *args: App._useEventKey('char'),
+ lambda *args: App._useEventKey('char'))
+
+ @_safeMethod
+ def _keyPressedWrapper(app, event):
+ event = App.KeyEventWrapper(event)
+ if (event.key == 'control-s'):
+ app.saveSnapshot()
+ elif (event.key == 'control-p'):
+ app._togglePaused()
+ app._redrawAllWrapper()
+ elif (event.key == 'control-q'):
+ app.quit()
+ elif (event.key == 'control-x'):
+ os._exit(0) # hard exit avoids tkinter error messages
+ elif (app._running and
+ (not app._paused) and
+ app._methodIsOverridden('keyPressed') and
+ (not event.key == 'Modifier_Key')):
+ app.keyPressed(event)
+ app._redrawAllWrapper()
+
+ @_safeMethod
+ def _keyReleasedWrapper(app, event):
+ if (not app._running) or app._paused or (not app._methodIsOverridden('keyReleased')): return
+ event = App.KeyEventWrapper(event)
+ if (not event.key == 'Modifier_Key'):
+ app.keyReleased(event)
+ app._redrawAllWrapper()
+
+ @_safeMethod
+ def _mousePressedWrapper(app, event):
+ if (not app._running) or app._paused: return
+ if ((event.x < 0) or (event.x > app.width) or
+ (event.y < 0) or (event.y > app.height)):
+ app._mousePressedOutsideWindow = True
+ else:
+ app._mousePressedOutsideWindow = False
+ app._mouseIsPressed = True
+ app._lastMousePosn = (event.x, event.y)
+ if (app._methodIsOverridden('mousePressed')):
+ app.mousePressed(event)
+ app._redrawAllWrapper()
+
+ @_safeMethod
+ def _mouseReleasedWrapper(app, event):
+ if (not app._running) or app._paused: return
+ app._mouseIsPressed = False
+ if app._mousePressedOutsideWindow:
+ app._mousePressedOutsideWindow = False
+ app._sizeChangedWrapper()
+ else:
+ app._lastMousePosn = (event.x, event.y)
+ if (app._methodIsOverridden('mouseReleased')):
+ app.mouseReleased(event)
+ app._redrawAllWrapper()
+
+ @_safeMethod
+ def _timerFiredWrapper(app):
+ if (not app._running) or (not app._methodIsOverridden('timerFired')): return
+ if (not app._paused):
+ app.timerFired()
+ app._redrawAllWrapper()
+ app._deferredMethodCall(afterId='_timerFiredWrapper', afterDelay=app.timerDelay, afterFn=app._timerFiredWrapper)
+
+ @_safeMethod
+ def _sizeChangedWrapper(app, event=None):
+ if (not app._running): return
+ if (event and ((event.width < 2) or (event.height < 2))): return
+ if (app._mousePressedOutsideWindow): return
+ app.width,app.height,app.winx,app.winy = [int(v) for v in app._root.winfo_geometry().replace('x','+').split('+')]
+ if (app._lastWindowDims is None):
+ app._lastWindowDims = (app.width, app.height, app.winx, app.winy)
+ else:
+ newDims =(app.width, app.height, app.winx, app.winy)
+ if (app._lastWindowDims != newDims):
+ app._lastWindowDims = newDims
+ app.updateTitle()
+ app.sizeChanged()
+ app._deferredRedrawAll() # avoid resize crashing on some platforms
+
+ @_safeMethod
+ def _mouseMotionWrapper(app):
+ if (not app._running): return
+ mouseMovedExists = app._methodIsOverridden('mouseMoved')
+ mouseDraggedExists = app._methodIsOverridden('mouseDragged')
+ if ((not app._paused) and
+ (not app._mousePressedOutsideWindow) and
+ (((not app._mouseIsPressed) and mouseMovedExists) or
+ (app._mouseIsPressed and mouseDraggedExists))):
+ class MouseMotionEvent(object): pass
+ event = MouseMotionEvent()
+ root = app._root
+ event.x = root.winfo_pointerx() - root.winfo_rootx()
+ event.y = root.winfo_pointery() - root.winfo_rooty()
+ if ((app._lastMousePosn != (event.x, event.y)) and
+ (event.x >= 0) and (event.x <= app.width) and
+ (event.y >= 0) and (event.y <= app.height)):
+ if (app._mouseIsPressed): app.mouseDragged(event)
+ else: app.mouseMoved(event)
+ app._lastMousePosn = (event.x, event.y)
+ app._redrawAllWrapper()
+ if (mouseMovedExists or mouseDraggedExists):
+ app._deferredMethodCall(afterId='mouseMotionWrapper', afterDelay=app.mouseMovedDelay, afterFn=app._mouseMotionWrapper)
+
+ def updateTitle(app):
+ app._title = app._title or type(app).__name__
+ app._root.title(f'{app._title} ({app.width} x {app.height})')
+
+ def getQuitMessage(app):
+ appLabel = type(app).__name__
+ if (app._title != appLabel):
+ if (app._title.startswith(appLabel)):
+ appLabel = app._title
+ else:
+ appLabel += f" '{app._title}'"
+ return f"*** Closing {appLabel}. Bye! ***\n"
+
+ def _showRootWindow(app):
+ root = app._root
+ root.update(); root.deiconify(); root.lift(); root.focus()
+
+ def _hideRootWindow(app):
+ root = app._root
+ root.withdraw()
+
+ @_safeMethod
+ def run(app):
+ app._mouseIsPressed = False
+ app._lastMousePosn = (-1, -1)
+ app._lastWindowDims= None # set in sizeChangedWrapper
+ app._afterIdMap = dict()
+ # create the singleton root window
+ if (App._theRoot is None):
+ App._theRoot = Tk()
+ App._theRoot.createcommand('exit', lambda: '') # when user enters cmd-q, ignore here (handled in keyPressed)
+ App._theRoot.protocol('WM_DELETE_WINDOW', lambda: App._theRoot.app.quit()) # when user presses 'x' in title bar
+ App._theRoot.bind("<Button-1>", lambda event: App._theRoot.app._mousePressedWrapper(event))
+ App._theRoot.bind("<B1-ButtonRelease>", lambda event: App._theRoot.app._mouseReleasedWrapper(event))
+ App._theRoot.bind("<KeyPress>", lambda event: App._theRoot.app._keyPressedWrapper(event))
+ App._theRoot.bind("<KeyRelease>", lambda event: App._theRoot.app._keyReleasedWrapper(event))
+ App._theRoot.bind("<Configure>", lambda event: App._theRoot.app._sizeChangedWrapper(event))
+ else:
+ App._theRoot.canvas.destroy()
+ app._root = root = App._theRoot # singleton root!
+ root.app = app
+ root.geometry(f'{app.width}x{app.height}+{app.winx}+{app.winy}')
+ app.updateTitle()
+ # create the canvas
+ root.canvas = app._canvas = WrappedCanvas(app)
+ app._canvas.pack(fill=BOTH, expand=YES)
+ # initialize, start the timer, and launch the app
+ app._running = True
+ app._paused = False
+ app._appStartedWrapper()
+ app._timerFiredWrapper()
+ app._mouseMotionWrapper()
+ app._showRootWindow()
+ root.mainloop()
+ app._hideRootWindow()
+ app._running = False
+ for afterId in app._afterIdMap: app._root.after_cancel(app._afterIdMap[afterId])
+ app._afterIdMap.clear() # for safety
+ app.appStopped()
+ print(app.getQuitMessage())
+
+####################################
+# TopLevelApp:
+# (with top-level functions not subclassses and methods)
+####################################
+
+class TopLevelApp(App):
+ _apps = dict() # maps fnPrefix to app
+
+ def __init__(app, fnPrefix='', **kwargs):
+ if (fnPrefix in TopLevelApp._apps):
+ print(f'Quitting previous version of {fnPrefix} TopLevelApp.')
+ TopLevelApp._apps[fnPrefix].quit()
+ if ((fnPrefix != '') and ('title' not in kwargs)):
+ kwargs['title'] = f"TopLevelApp '{fnPrefix}'"
+ TopLevelApp._apps[fnPrefix] = app
+ app._fnPrefix = fnPrefix
+ app._callersGlobals = inspect.stack()[1][0].f_globals
+ super().__init__(**kwargs)
+
+ def _callFn(app, fn, *args):
+ fn = app._fnPrefix + fn
+ if (fn in app._callersGlobals): app._callersGlobals[fn](*args)
+
+ def redrawAll(app, canvas): app._callFn('redrawAll', app, canvas)
+ def appStarted(app): app._callFn('appStarted', app)
+ def appStopped(app): app._callFn('appStopped', app)
+ def keyPressed(app, event): app._callFn('keyPressed', app, event)
+ def keyReleased(app, event): app._callFn('keyReleased', app, event)
+ def mousePressed(app, event): app._callFn('mousePressed', app, event)
+ def mouseReleased(app, event): app._callFn('mouseReleased', app, event)
+ def mouseMoved(app, event): app._callFn('mouseMoved', app, event)
+ def mouseDragged(app, event): app._callFn('mouseDragged', app, event)
+ def timerFired(app): app._callFn('timerFired', app)
+ def sizeChanged(app): app._callFn('sizeChanged', app)
+
+####################################
+# ModalApp + Mode:
+####################################
+
+class ModalApp(App):
+ def __init__(app, activeMode=None, **kwargs):
+ app._running = False
+ app._activeMode = None
+ app.setActiveMode(activeMode)
+ super().__init__(**kwargs)
+
+ def setActiveMode(app, mode):
+ if (mode == None): mode = Mode() # default empty mode
+ if (not isinstance(mode, Mode)): raise Exception('activeMode must be a mode!')
+ if (mode.app not in [None, app]): raise Exception('Modes cannot be added to two different apps!')
+ if (app._activeMode != mode):
+ mode.app = app
+ if (app._activeMode != None): app._activeMode.modeDeactivated()
+ app._activeMode = mode
+ if (app._running): app.startActiveMode()
+
+ def startActiveMode(app):
+ app._activeMode.width, app._activeMode.height = app.width, app.height
+ if (not app._activeMode._appStartedCalled):
+ app._activeMode.appStarted() # called once per mode
+ app._activeMode._appStartedCalled = True
+ app._activeMode.modeActivated() # called each time a mode is activated
+ app._redrawAllWrapper()
+
+ def redrawAll(app, canvas):
+ if (app._activeMode != None): app._activeMode.redrawAll(canvas)
+ def appStarted(app):
+ if (app._activeMode != None): app.startActiveMode()
+ def appStopped(app):
+ if (app._activeMode != None): app._activeMode.modeDeactivated()
+ def keyPressed(app, event):
+ if (app._activeMode != None): app._activeMode.keyPressed(event)
+ def keyReleased(app, event):
+ if (app._activeMode != None): app._activeMode.keyReleased(event)
+ def mousePressed(app, event):
+ if (app._activeMode != None): app._activeMode.mousePressed(event)
+ def mouseReleased(app, event):
+ if (app._activeMode != None): app._activeMode.mouseReleased(event)
+ def mouseMoved(app, event):
+ if (app._activeMode != None): app._activeMode.mouseMoved(event)
+ def mouseDragged(app, event):
+ if (app._activeMode != None): app._activeMode.mouseDragged(event)
+ def timerFired(app):
+ if (app._activeMode != None): app._activeMode.timerFired()
+ def sizeChanged(app):
+ if (app._activeMode != None):
+ app._activeMode.width, app._activeMode.height = app.width, app.height
+ app._activeMode.sizeChanged()
+
+class Mode(App):
+ def __init__(mode, **kwargs):
+ mode.app = None
+ mode._appStartedCalled = False
+ super().__init__(autorun=False, **kwargs)
+ def modeActivated(mode): pass
+ def modeDeactivated(mode): pass
+ def loadImage(mode, path=None): return mode.app.loadImage(path)
+
+####################################
+# runApp()
+####################################
+
+'''
+def showGraphics(drawFn, **kwargs):
+ class GraphicsApp(App):
+ def __init__(app, **kwargs):
+ if ('title' not in kwargs):
+ kwargs['title'] = drawFn.__name__
+ super().__init__(**kwargs)
+ def redrawAll(app, canvas):
+ drawFn(app, canvas)
+ app = GraphicsApp(**kwargs)
+'''
+runApp = TopLevelApp
+
+print(f'Loaded cmu_112_graphics version {App.version} (last updated {App.lastUpdated})')
+
+if (__name__ == '__main__'):
+ try: import cmu_112_graphics_tests
+ except: pass
+
+
+################################################################################
+file = str(input("Please type the exact file name of the match you want to visualize. Ensure that your match data and this file are in the same directory:\n"))
+
+f = open(file)
+json_out = json.load(f)
+lines = json_out['gamedata']
+f.close()
+
+index = json_out['seat'] - 1
+player = ['Trapper','Scotty'][::-1][index]
+
+turns = len(lines)
+
+gap = int(input('Time between frames (ms): '))
+
+#from cmu_112_graphics import *
+
+size = 15
+cs = 40
+
+def appStarted(app):
+ app.playing = True
+ app.timerDelay = gap
+ app.curr = 0
+ app.lastUpd = time.time()
+ app.error = ''
+
+def timerFired(app):
+ # This is a Controller
+ doStep(app)
+
+def doStep(app):
+ if app.playing:
+ if app.curr < len(lines) - 1:
+ app.curr += 1
+
+ line = lines[app.curr]
+
+ app.error = ''
+ for thing in line['moves']:
+ if thing['seat'] == index + 1:
+ if 'error' in thing:
+ app.error = thing['error']
+
+ if app.error != '':
+ app.playing = False
+
+
+def keyPressed(app, event):
+ if event.key == 'Right' and app.curr + 1 < len(lines):
+ app.curr += 1
+ if event.key == 'Left' and app.curr > 0:
+ app.curr -= 1
+ if event.key == 'Space':
+ app.playing = not app.playing
+
+def redrawAll(app, canvas):
+ states = lines[app.curr]['state']
+
+ for i in range(size):
+ for j in range(size):
+ #cell = states[14-i][j]
+ cell = states[14-j][i]
+
+ if cell == 1:
+ color = 'orange'
+ elif cell == 2:
+ color = 'green'
+ elif cell == 3:
+ color = 'blue'
+ elif cell == 0:
+ color = 'white'
+ canvas.create_rectangle(cs * i + 5, cs * j + 5, cs * i + cs + 5, cs * j + cs + 5,fill=color)
+
+ if app.playing:
+ canvas.create_text(cs * size + 200, 50, text=f'Currently running',font='calibri')
+ canvas.create_text(cs * size + 200, 75, text=f'Press Space to Pause',font='calibri')
+ else:
+ canvas.create_text(cs * size + 200, 50, text=f'Currently paused',font='calibri')
+ canvas.create_text(cs * size + 200, 75, text=f'Press Space to Run',font='calibri')
+ canvas.create_text(cs * size + 200, 100, text=f'Press Left/Right to advance frame by frame',font='calibri')
+
+
+
+ if app.error:
+ canvas.create_text(cs * size + 200, 500, text=f'Program Error:',font='calibri',fill='red')
+ canvas.create_text(cs * size + 200, 525, text=f'{app.error}',font='calibri 10',fill='red')
+ canvas.create_text(cs * size + 200, 550, text=f'(autoplay stopped)',font='calibri 10',fill='red')
+
+ canvas.create_text(cs * size + 200, 200,text=f'Frame {app.curr + 1} of {len(lines)}',font='calibri')
+ canvas.create_text(cs * size + 200, 300,text=f'You are {player}',font='calibri')
+
+def main():
+ runApp(width=cs * size + 400, height=cs * size + 10)
+
+main() \ No newline at end of file
diff --git a/ai/3.py b/ai/3.py
index 1fff9b7..7815423 100644..100755
--- a/ai/3.py
+++ b/ai/3.py
@@ -1,3 +1,4 @@
+#!/usr/bin/python
# Spaceship Starter File
# NOTE: You can run this file locally to test if your program is working.
diff --git a/ai/3vi.py b/ai/3vi.py
new file mode 100755
index 0000000..84456b2
--- /dev/null
+++ b/ai/3vi.py
@@ -0,0 +1,870 @@
+#!/usr/bin/python
+import json,time
+
+# Problem Specific visualizer code starts at line 748
+################################################################################
+# cmu_112_graphics.py
+# version 0.8.6
+
+# Pre-release for CMU 15-112-f20
+
+# Require Python 3.6 or later
+import sys
+if ((sys.version_info[0] != 3) or (sys.version_info[1] < 6)):
+ raise Exception('cmu_112_graphics.py requires Python version 3.6 or later.')
+
+# Track version and file update timestamp
+import datetime
+MAJOR_VERSION = 0
+MINOR_VERSION = 8.6 # version 0.8.6
+LAST_UPDATED = datetime.date(year=2020, month=2, day=24)
+
+# Pending changes:
+# * Fix Windows-only bug: Position popup dialog box over app window (already works fine on Macs)
+# * Add documentation
+# * integrate sounds (probably from pyGame)
+# * Improved methodIsOverridden to TopLevelApp and ModalApp
+# * Save to animated gif and/or mp4 (with audio capture?)
+
+# Deferred changes:
+# * replace/augment tkinter canvas with PIL/Pillow imageDraw (perhaps with our own fn names)
+# * use snake_case and CapWords
+
+# Chages in v0.8.6
+# * f20
+
+# Chages in v0.8.5
+# * Support loadImage from Modes
+
+# Chages in v0.8.3 + v0.8.4
+# * Use default empty Mode if none is provided
+# * Add KeyRelease event binding
+# * Drop user32.SetProcessDPIAware (caused window to be really tiny on some Windows machines)
+
+# Changes in v0.8.1 + v0.8.2
+# * print version number and last-updated date on load
+# * restrict modifiers to just control key (was confusing with NumLock, etc)
+# * replace hasModifiers with 'control-' prefix, as in 'control-A'
+# * replace app._paused with app.paused, etc (use app._ for private variables)
+# * use improved ImageGrabber import for linux
+
+# Changes in v0.8.0
+# * suppress more modifier keys (Super_L, Super_R, ...)
+# * raise exception on event.keysym or event.char + works with key = 'Enter'
+# * remove tryToInstall
+
+# Changes in v0.7.4
+# * renamed drawAll back to redrawAll :-)
+
+# Changes in v0.7.3
+# * Ignore mousepress-drag-release and defer configure events for drags in titlebar
+# * Extend deferredRedrawAll to 100ms with replace=True and do not draw while deferred
+# (together these hopefully fix Windows-only bug: file dialog makes window not moveable)
+# * changed sizeChanged to not take event (use app.width and app.height)
+
+# Changes in v0.7.2
+# * Singleton App._theRoot instance (hopefully fixes all those pesky Tkinter errors-on-exit)
+# * Use user32.SetProcessDPIAware to get resolution of screen grabs right on Windows-only (fine on Macs)
+# * Replaces showGraphics() with runApp(...), which is a veneer for App(...) [more intuitive for pre-OOP part of course]
+# * Fixes/updates images:
+# * disallows loading images in redrawAll (raises exception)
+# * eliminates cache from loadImage
+# * eliminates app.getTkinterImage, so user now directly calls ImageTk.PhotoImage(image))
+# * also create_image allows magic pilImage=image instead of image=ImageTk.PhotoImage(app.image)
+
+# Changes in v0.7.1
+# * Added keyboard shortcut:
+# * cmd/ctrl/alt-x: hard exit (uses os._exit() to exit shell without tkinter error messages)
+# * Fixed bug: shortcut keys stopped working after an MVC violation (or other exception)
+# * In app.saveSnapshot(), add .png to path if missing
+# * Added: Print scripts to copy-paste into shell to install missing modules (more automated approaches proved too brittle)
+
+# Changes in v0.7
+# * Added some image handling (requires PIL (retained) and pyscreenshot (later removed):
+# * app.loadImage() # loads PIL/Pillow image from file, with file dialog, or from URL (http or https)
+# * app.scaleImage() # scales a PIL/Pillow image
+# * app.getTkinterImage() # converts PIL/Pillow image to Tkinter PhotoImage for use in create_image(...)
+# * app.getSnapshot() # get a snapshot of the canvas as a PIL/Pillow image
+# * app.saveSnapshot() # get and save a snapshot
+# * Added app._paused, app.togglePaused(), and paused highlighting (red outline around canvas when paused)
+# * Added keyboard shortcuts:
+# * cmd/ctrl/alt-s: save a snapshot
+# * cmd/ctrl/alt-p: pause/unpause
+# * cmd/ctrl/alt-q: quit
+
+# Changes in v0.6:
+# * Added fnPrefix option to TopLevelApp (so multiple TopLevelApp's can be in one file)
+# * Added showGraphics(drawFn) (for graphics-only drawings before we introduce animations)
+
+# Changes in v0.5:
+# * Added:
+# * app.winx and app.winy (and add winx,winy parameters to app.__init__, and sets these on configure events)
+# * app.setSize(width, height)
+# * app.setPosition(x, y)
+# * app.quit()
+# * app.showMessage(message)
+# * app.getUserInput(prompt)
+# * App.lastUpdated (instance of datetime.date)
+# * Show popup dialog box on all exceptions (not just for MVC violations)
+# * Draw (in canvas) "Exception! App Stopped! (See console for details)" for any exception
+# * Replace callUserMethod() with more-general @_safeMethod decorator (also handles exceptions outside user methods)
+# * Only include lines from user's code (and not our framework nor tkinter) in stack traces
+# * Require Python version (3.6 or greater)
+
+# Changes in v0.4:
+# * Added __setattr__ to enforce Type 1A MVC Violations (setting app.x in redrawAll) with better stack trace
+# * Added app._deferredRedrawAll() (avoids resizing drawing/crashing bug on some platforms)
+# * Added deferredMethodCall() and app._afterIdMap to generalize afterId handling
+# * Use (_ is None) instead of (_ == None)
+
+# Changes in v0.3:
+# * Fixed "event not defined" bug in sizeChanged handlers.
+# * draw "MVC Violation" on Type 2 violation (calling draw methods outside redrawAll)
+
+# Changes in v0.2:
+# * Handles another MVC violation (now detects drawing on canvas outside of redrawAll)
+# * App stops running when an exception occurs (in user code) (stops cascading errors)
+
+# Changes in v0.1:
+# * OOPy + supports inheritance + supports multiple apps in one file + etc
+# * uses import instead of copy-paste-edit starter code + no "do not edit code below here!"
+# * no longer uses Struct (which was non-Pythonic and a confusing way to sort-of use OOP)
+# * Includes an early version of MVC violation handling (detects model changes in redrawAll)
+# * added events:
+# * appStarted (no init-vs-__init__ confusion)
+# * appStopped (for cleanup)
+# * keyReleased (well, sort of works) + mouseReleased
+# * mouseMoved + mouseDragged
+# * sizeChanged (when resizing window)
+# * improved key names (just use event.key instead of event.char and/or event.keysym + use names for 'Enter', 'Escape', ...)
+# * improved function names (renamed redrawAll to drawAll)
+# * improved (if not perfect) exiting without that irksome Tkinter error/bug
+# * app has a title in the titlebar (also shows window's dimensions)
+# * supports Modes and ModalApp (see ModalApp and Mode, and also see TestModalApp example)
+# * supports TopLevelApp (using top-level functions instead of subclasses and methods)
+# * supports version checking with App.majorVersion, App.minorVersion, and App.version
+# * logs drawing calls to support autograding views (still must write that autograder, but this is a very helpful first step)
+
+from tkinter import *
+from tkinter import messagebox, simpledialog, filedialog
+import inspect, copy, traceback
+import sys, os
+from io import BytesIO
+
+def failedImport(importName, installName=None): pass
+
+try: from PIL import Image, ImageTk
+except ModuleNotFoundError: failedImport('PIL', 'pillow')
+
+if sys.platform.startswith('linux'):
+ try: import pyscreenshot as ImageGrabber
+ except ModuleNotFoundError: failedImport('pyscreenshot')
+else:
+ try: from PIL import ImageGrab as ImageGrabber
+ except ModuleNotFoundError: pass # Our PIL warning is already printed above
+
+try: import requests
+except ModuleNotFoundError: failedImport('requests')
+
+def getHash(obj):
+ # This is used to detect MVC violations in redrawAll
+ # @TODO: Make this more robust and efficient
+ try:
+ return getHash(obj.__dict__)
+ except:
+ if (isinstance(obj, list)): return getHash(tuple([getHash(v) for v in obj]))
+ elif (isinstance(obj, set)): return getHash(sorted(obj))
+ elif (isinstance(obj, dict)): return getHash(tuple([obj[key] for key in sorted(obj)]))
+ else:
+ try: return hash(obj)
+ except: return getHash(repr(obj))
+
+class WrappedCanvas(Canvas):
+ # Enforces MVC: no drawing outside calls to redrawAll
+ # Logs draw calls (for autograder) in canvas.loggedDrawingCalls
+ def __init__(wrappedCanvas, app):
+ wrappedCanvas.loggedDrawingCalls = [ ]
+ wrappedCanvas.logDrawingCalls = True
+ wrappedCanvas.inRedrawAll = False
+ wrappedCanvas.app = app
+ super().__init__(app._root, width=app.width, height=app.height)
+
+ def log(self, methodName, args, kwargs):
+ if (not self.inRedrawAll):
+ self.app._mvcViolation('you may not use the canvas (the view) outside of redrawAll')
+ if (self.logDrawingCalls):
+ self.loggedDrawingCalls.append((methodName, args, kwargs))
+
+ def create_arc(self, *args, **kwargs): self.log('create_arc', args, kwargs); return super().create_arc(*args, **kwargs)
+ def create_bitmap(self, *args, **kwargs): self.log('create_bitmap', args, kwargs); return super().create_bitmap(*args, **kwargs)
+ def create_line(self, *args, **kwargs): self.log('create_line', args, kwargs); return super().create_line(*args, **kwargs)
+ def create_oval(self, *args, **kwargs): self.log('create_oval', args, kwargs); return super().create_oval(*args, **kwargs)
+ def create_polygon(self, *args, **kwargs): self.log('create_polygon', args, kwargs); return super().create_polygon(*args, **kwargs)
+ def create_rectangle(self, *args, **kwargs): self.log('create_rectangle', args, kwargs); return super().create_rectangle(*args, **kwargs)
+ def create_text(self, *args, **kwargs): self.log('create_text', args, kwargs); return super().create_text(*args, **kwargs)
+ def create_window(self, *args, **kwargs): self.log('create_window', args, kwargs); return super().create_window(*args, **kwargs)
+
+ def create_image(self, *args, **kwargs):
+ self.log('create_image', args, kwargs);
+ usesImage = 'image' in kwargs
+ usesPilImage = 'pilImage' in kwargs
+ if ((not usesImage) and (not usesPilImage)):
+ raise Exception('create_image requires an image to draw')
+ elif (usesImage and usesPilImage):
+ raise Exception('create_image cannot use both an image and a pilImage')
+ elif (usesPilImage):
+ pilImage = kwargs['pilImage']
+ del kwargs['pilImage']
+ if (not isinstance(pilImage, Image.Image)):
+ raise Exception('create_image: pilImage value is not an instance of a PIL/Pillow image')
+ image = ImageTk.PhotoImage(pilImage)
+ else:
+ image = kwargs['image']
+ if (isinstance(image, Image.Image)):
+ raise Exception('create_image: image must not be an instance of a PIL/Pillow image\n' +
+ 'You perhaps meant to convert from PIL to Tkinter, like so:\n' +
+ ' canvas.create_image(x, y, image=ImageTk.PhotoImage(image))')
+ kwargs['image'] = image
+ return super().create_image(*args, **kwargs)
+
+class App(object):
+ majorVersion = MAJOR_VERSION
+ minorVersion = MINOR_VERSION
+ version = f'{majorVersion}.{minorVersion}'
+ lastUpdated = LAST_UPDATED
+ _theRoot = None # singleton Tkinter root object
+
+ ####################################
+ # User Methods:
+ ####################################
+ def redrawAll(app, canvas): pass # draw (view) the model in the canvas
+ def appStarted(app): pass # initialize the model (app.xyz)
+ def appStopped(app): pass # cleanup after app is done running
+ def keyPressed(app, event): pass # use event.key
+ def keyReleased(app, event): pass # use event.key
+ def mousePressed(app, event): pass # use event.x and event.y
+ def mouseReleased(app, event): pass # use event.x and event.y
+ def mouseMoved(app, event): pass # use event.x and event.y
+ def mouseDragged(app, event): pass # use event.x and event.y
+ def timerFired(app): pass # respond to timer events
+ def sizeChanged(app): pass # respond to window size changes
+
+ ####################################
+ # Implementation:
+ ####################################
+
+ def __init__(app, width=300, height=300, x=0, y=0, title=None, autorun=True, mvcCheck=True, logDrawingCalls=True):
+ app.winx, app.winy, app.width, app.height = x, y, width, height
+ app.timerDelay = 100 # milliseconds
+ app.mouseMovedDelay = 50 # ditto
+ app._title = title
+ app._mvcCheck = mvcCheck
+ app._logDrawingCalls = logDrawingCalls
+ app._running = app._paused = False
+ app._mousePressedOutsideWindow = False
+ if autorun: app.run()
+
+ def setSize(app, width, height):
+ app._root.geometry(f'{width}x{height}')
+
+ def setPosition(app, x, y):
+ app._root.geometry(f'+{x}+{y}')
+
+ def showMessage(app, message):
+ messagebox.showinfo('showMessage', message, parent=app._root)
+
+ def getUserInput(app, prompt):
+ return simpledialog.askstring('getUserInput', prompt)
+
+ def loadImage(app, path=None):
+ if (app._canvas.inRedrawAll):
+ raise Exception('Cannot call loadImage in redrawAll')
+ if (path is None):
+ path = filedialog.askopenfilename(initialdir=os.getcwd(), title='Select file: ',filetypes = (('Image files','*.png *.gif *.jpg'),('all files','*.*')))
+ if (not path): return None
+ if (path.startswith('http')):
+ response = requests.request('GET', path) # path is a URL!
+ image = Image.open(BytesIO(response.content))
+ else:
+ image = Image.open(path)
+ return image
+
+ def scaleImage(app, image, scale, antialias=False):
+ # antialiasing is higher-quality but slower
+ resample = Image.ANTIALIAS if antialias else Image.NEAREST
+ return image.resize((round(image.width*scale), round(image.height*scale)), resample=resample)
+
+ def getSnapshot(app):
+ app._showRootWindow()
+ x0 = app._root.winfo_rootx() + app._canvas.winfo_x()
+ y0 = app._root.winfo_rooty() + app._canvas.winfo_y()
+ result = ImageGrabber.grab((x0,y0,x0+app.width,y0+app.height))
+ return result
+
+ def saveSnapshot(app):
+ path = filedialog.asksaveasfilename(initialdir=os.getcwd(), title='Select file: ',filetypes = (('png files','*.png'),('all files','*.*')))
+ if (path):
+ # defer call to let filedialog close (and not grab those pixels)
+ if (not path.endswith('.png')): path += '.png'
+ app._deferredMethodCall(afterId='saveSnapshot', afterDelay=0, afterFn=lambda:app.getSnapshot().save(path))
+
+ def _togglePaused(app):
+ app._paused = not app._paused
+
+ def quit(app):
+ app._running = False
+ app._root.quit() # break out of root.mainloop() without closing window!
+
+ def __setattr__(app, attr, val):
+ d = app.__dict__
+ d[attr] = val
+ canvas = d.get('_canvas', None)
+ if (d.get('running', False) and
+ d.get('mvcCheck', False) and
+ (canvas is not None) and
+ canvas.inRedrawAll):
+ app._mvcViolation(f'you may not change app.{attr} in the model while in redrawAll (the view)')
+
+ def _printUserTraceback(app, exception, tb):
+ stack = traceback.extract_tb(tb)
+ lines = traceback.format_list(stack)
+ inRedrawAllWrapper = False
+ printLines = [ ]
+ for line in lines:
+ if (('"cmu_112_graphics.py"' not in line) and
+ ('/cmu_112_graphics.py' not in line) and
+ ('\\cmu_112_graphics.py' not in line) and
+ ('/tkinter/' not in line) and
+ ('\\tkinter\\' not in line)):
+ printLines.append(line)
+ if ('redrawAllWrapper' in line):
+ inRedrawAllWrapper = True
+ if (len(printLines) == 0):
+ # No user code in trace, so we have to use all the code (bummer),
+ # but not if we are in a redrawAllWrapper...
+ if inRedrawAllWrapper:
+ printLines = [' No traceback available. Error occurred in redrawAll.\n']
+ else:
+ printLines = lines
+ print('Traceback (most recent call last):')
+ for line in printLines: print(line, end='')
+ print(f'Exception: {exception}')
+
+ def _safeMethod(appMethod):
+ def m(*args, **kwargs):
+ app = args[0]
+ try:
+ return appMethod(*args, **kwargs)
+ except Exception as e:
+ app._running = False
+ app._printUserTraceback(e, sys.exc_info()[2])
+ if ('_canvas' in app.__dict__):
+ app._canvas.inRedrawAll = True # not really, but stops recursive MVC Violations!
+ app._canvas.create_rectangle(0, 0, app.width, app.height, fill=None, width=10, outline='red')
+ app._canvas.create_rectangle(10, app.height-50, app.width-10, app.height-10,
+ fill='white', outline='red', width=4)
+ app._canvas.create_text(app.width/2, app.height-40, text=f'Exception! App Stopped!', fill='red', font='Arial 12 bold')
+ app._canvas.create_text(app.width/2, app.height-20, text=f'See console for details', fill='red', font='Arial 12 bold')
+ app._canvas.update()
+ app.showMessage(f'Exception: {e}\nClick ok then see console for details.')
+ return m
+
+ def _methodIsOverridden(app, methodName):
+ return (getattr(type(app), methodName) is not getattr(App, methodName))
+
+ def _mvcViolation(app, errMsg):
+ app._running = False
+ raise Exception('MVC Violation: ' + errMsg)
+
+ @_safeMethod
+ def _redrawAllWrapper(app):
+ if (not app._running): return
+ if ('deferredRedrawAll' in app._afterIdMap): return # wait for pending call
+ app._canvas.inRedrawAll = True
+ app._canvas.delete(ALL)
+ width,outline = (10,'red') if app._paused else (0,'white')
+ app._canvas.create_rectangle(0, 0, app.width, app.height, fill='white', width=width, outline=outline)
+ app._canvas.loggedDrawingCalls = [ ]
+ app._canvas.logDrawingCalls = app._logDrawingCalls
+ hash1 = getHash(app) if app._mvcCheck else None
+ try:
+ app.redrawAll(app._canvas)
+ hash2 = getHash(app) if app._mvcCheck else None
+ if (hash1 != hash2):
+ app._mvcViolation('you may not change the app state (the model) in redrawAll (the view)')
+ finally:
+ app._canvas.inRedrawAll = False
+ app._canvas.update()
+
+ def _deferredMethodCall(app, afterId, afterDelay, afterFn, replace=False):
+ def afterFnWrapper():
+ app._afterIdMap.pop(afterId, None)
+ afterFn()
+ id = app._afterIdMap.get(afterId, None)
+ if ((id is None) or replace):
+ if id: app._root.after_cancel(id)
+ app._afterIdMap[afterId] = app._root.after(afterDelay, afterFnWrapper)
+
+ def _deferredRedrawAll(app):
+ app._deferredMethodCall(afterId='deferredRedrawAll', afterDelay=100, afterFn=app._redrawAllWrapper, replace=True)
+
+ @_safeMethod
+ def _appStartedWrapper(app):
+ app.appStarted()
+ app._redrawAllWrapper()
+
+ _keyNameMap = { '\t':'Tab', '\n':'Enter', '\r':'Enter', '\b':'Backspace',
+ chr(127):'Delete', chr(27):'Escape', ' ':'Space' }
+
+ @staticmethod
+ def _useEventKey(attr):
+ raise Exception(f'Use event.key instead of event.{attr}')
+
+ @staticmethod
+ def _getEventKeyInfo(event, keysym, char):
+ key = c = char
+ hasControlKey = (event.state & 0x4 != 0)
+ if ((c in [None, '']) or (len(c) > 1) or (ord(c) > 255)):
+ key = keysym
+ if (key.endswith('_L') or
+ key.endswith('_R') or
+ key.endswith('_Lock')):
+ key = 'Modifier_Key'
+ elif (c in App._keyNameMap):
+ key = App._keyNameMap[c]
+ elif ((len(c) == 1) and (1 <= ord(c) <= 26)):
+ key = chr(ord('a')-1 + ord(c))
+ hasControlKey = True
+ if hasControlKey and (len(key) == 1):
+ # don't add control- prefix to Enter, Tab, Escape, ...
+ key = 'control-' + key
+ return key
+
+ class KeyEventWrapper(Event):
+ def __init__(self, event):
+ keysym, char = event.keysym, event.char
+ del event.keysym
+ del event.char
+ for key in event.__dict__:
+ if (not key.startswith('__')):
+ self.__dict__[key] = event.__dict__[key]
+ self.key = App._getEventKeyInfo(event, keysym, char)
+ keysym = property(lambda *args: App._useEventKey('keysym'),
+ lambda *args: App._useEventKey('keysym'))
+ char = property(lambda *args: App._useEventKey('char'),
+ lambda *args: App._useEventKey('char'))
+
+ @_safeMethod
+ def _keyPressedWrapper(app, event):
+ event = App.KeyEventWrapper(event)
+ if (event.key == 'control-s'):
+ app.saveSnapshot()
+ elif (event.key == 'control-p'):
+ app._togglePaused()
+ app._redrawAllWrapper()
+ elif (event.key == 'control-q'):
+ app.quit()
+ elif (event.key == 'control-x'):
+ os._exit(0) # hard exit avoids tkinter error messages
+ elif (app._running and
+ (not app._paused) and
+ app._methodIsOverridden('keyPressed') and
+ (not event.key == 'Modifier_Key')):
+ app.keyPressed(event)
+ app._redrawAllWrapper()
+
+ @_safeMethod
+ def _keyReleasedWrapper(app, event):
+ if (not app._running) or app._paused or (not app._methodIsOverridden('keyReleased')): return
+ event = App.KeyEventWrapper(event)
+ if (not event.key == 'Modifier_Key'):
+ app.keyReleased(event)
+ app._redrawAllWrapper()
+
+ @_safeMethod
+ def _mousePressedWrapper(app, event):
+ if (not app._running) or app._paused: return
+ if ((event.x < 0) or (event.x > app.width) or
+ (event.y < 0) or (event.y > app.height)):
+ app._mousePressedOutsideWindow = True
+ else:
+ app._mousePressedOutsideWindow = False
+ app._mouseIsPressed = True
+ app._lastMousePosn = (event.x, event.y)
+ if (app._methodIsOverridden('mousePressed')):
+ app.mousePressed(event)
+ app._redrawAllWrapper()
+
+ @_safeMethod
+ def _mouseReleasedWrapper(app, event):
+ if (not app._running) or app._paused: return
+ app._mouseIsPressed = False
+ if app._mousePressedOutsideWindow:
+ app._mousePressedOutsideWindow = False
+ app._sizeChangedWrapper()
+ else:
+ app._lastMousePosn = (event.x, event.y)
+ if (app._methodIsOverridden('mouseReleased')):
+ app.mouseReleased(event)
+ app._redrawAllWrapper()
+
+ @_safeMethod
+ def _timerFiredWrapper(app):
+ if (not app._running) or (not app._methodIsOverridden('timerFired')): return
+ if (not app._paused):
+ app.timerFired()
+ app._redrawAllWrapper()
+ app._deferredMethodCall(afterId='_timerFiredWrapper', afterDelay=app.timerDelay, afterFn=app._timerFiredWrapper)
+
+ @_safeMethod
+ def _sizeChangedWrapper(app, event=None):
+ if (not app._running): return
+ if (event and ((event.width < 2) or (event.height < 2))): return
+ if (app._mousePressedOutsideWindow): return
+ app.width,app.height,app.winx,app.winy = [int(v) for v in app._root.winfo_geometry().replace('x','+').split('+')]
+ if (app._lastWindowDims is None):
+ app._lastWindowDims = (app.width, app.height, app.winx, app.winy)
+ else:
+ newDims =(app.width, app.height, app.winx, app.winy)
+ if (app._lastWindowDims != newDims):
+ app._lastWindowDims = newDims
+ app.updateTitle()
+ app.sizeChanged()
+ app._deferredRedrawAll() # avoid resize crashing on some platforms
+
+ @_safeMethod
+ def _mouseMotionWrapper(app):
+ if (not app._running): return
+ mouseMovedExists = app._methodIsOverridden('mouseMoved')
+ mouseDraggedExists = app._methodIsOverridden('mouseDragged')
+ if ((not app._paused) and
+ (not app._mousePressedOutsideWindow) and
+ (((not app._mouseIsPressed) and mouseMovedExists) or
+ (app._mouseIsPressed and mouseDraggedExists))):
+ class MouseMotionEvent(object): pass
+ event = MouseMotionEvent()
+ root = app._root
+ event.x = root.winfo_pointerx() - root.winfo_rootx()
+ event.y = root.winfo_pointery() - root.winfo_rooty()
+ if ((app._lastMousePosn != (event.x, event.y)) and
+ (event.x >= 0) and (event.x <= app.width) and
+ (event.y >= 0) and (event.y <= app.height)):
+ if (app._mouseIsPressed): app.mouseDragged(event)
+ else: app.mouseMoved(event)
+ app._lastMousePosn = (event.x, event.y)
+ app._redrawAllWrapper()
+ if (mouseMovedExists or mouseDraggedExists):
+ app._deferredMethodCall(afterId='mouseMotionWrapper', afterDelay=app.mouseMovedDelay, afterFn=app._mouseMotionWrapper)
+
+ def updateTitle(app):
+ app._title = app._title or type(app).__name__
+ app._root.title(f'{app._title} ({app.width} x {app.height})')
+
+ def getQuitMessage(app):
+ appLabel = type(app).__name__
+ if (app._title != appLabel):
+ if (app._title.startswith(appLabel)):
+ appLabel = app._title
+ else:
+ appLabel += f" '{app._title}'"
+ return f"*** Closing {appLabel}. Bye! ***\n"
+
+ def _showRootWindow(app):
+ root = app._root
+ root.update(); root.deiconify(); root.lift(); root.focus()
+
+ def _hideRootWindow(app):
+ root = app._root
+ root.withdraw()
+
+ @_safeMethod
+ def run(app):
+ app._mouseIsPressed = False
+ app._lastMousePosn = (-1, -1)
+ app._lastWindowDims= None # set in sizeChangedWrapper
+ app._afterIdMap = dict()
+ # create the singleton root window
+ if (App._theRoot is None):
+ App._theRoot = Tk()
+ App._theRoot.createcommand('exit', lambda: '') # when user enters cmd-q, ignore here (handled in keyPressed)
+ App._theRoot.protocol('WM_DELETE_WINDOW', lambda: App._theRoot.app.quit()) # when user presses 'x' in title bar
+ App._theRoot.bind("<Button-1>", lambda event: App._theRoot.app._mousePressedWrapper(event))
+ App._theRoot.bind("<B1-ButtonRelease>", lambda event: App._theRoot.app._mouseReleasedWrapper(event))
+ App._theRoot.bind("<KeyPress>", lambda event: App._theRoot.app._keyPressedWrapper(event))
+ App._theRoot.bind("<KeyRelease>", lambda event: App._theRoot.app._keyReleasedWrapper(event))
+ App._theRoot.bind("<Configure>", lambda event: App._theRoot.app._sizeChangedWrapper(event))
+ else:
+ App._theRoot.canvas.destroy()
+ app._root = root = App._theRoot # singleton root!
+ root.app = app
+ root.geometry(f'{app.width}x{app.height}+{app.winx}+{app.winy}')
+ app.updateTitle()
+ # create the canvas
+ root.canvas = app._canvas = WrappedCanvas(app)
+ app._canvas.pack(fill=BOTH, expand=YES)
+ # initialize, start the timer, and launch the app
+ app._running = True
+ app._paused = False
+ app._appStartedWrapper()
+ app._timerFiredWrapper()
+ app._mouseMotionWrapper()
+ app._showRootWindow()
+ root.mainloop()
+ app._hideRootWindow()
+ app._running = False
+ for afterId in app._afterIdMap: app._root.after_cancel(app._afterIdMap[afterId])
+ app._afterIdMap.clear() # for safety
+ app.appStopped()
+ print(app.getQuitMessage())
+
+####################################
+# TopLevelApp:
+# (with top-level functions not subclassses and methods)
+####################################
+
+class TopLevelApp(App):
+ _apps = dict() # maps fnPrefix to app
+
+ def __init__(app, fnPrefix='', **kwargs):
+ if (fnPrefix in TopLevelApp._apps):
+ print(f'Quitting previous version of {fnPrefix} TopLevelApp.')
+ TopLevelApp._apps[fnPrefix].quit()
+ if ((fnPrefix != '') and ('title' not in kwargs)):
+ kwargs['title'] = f"TopLevelApp '{fnPrefix}'"
+ TopLevelApp._apps[fnPrefix] = app
+ app._fnPrefix = fnPrefix
+ app._callersGlobals = inspect.stack()[1][0].f_globals
+ super().__init__(**kwargs)
+
+ def _callFn(app, fn, *args):
+ fn = app._fnPrefix + fn
+ if (fn in app._callersGlobals): app._callersGlobals[fn](*args)
+
+ def redrawAll(app, canvas): app._callFn('redrawAll', app, canvas)
+ def appStarted(app): app._callFn('appStarted', app)
+ def appStopped(app): app._callFn('appStopped', app)
+ def keyPressed(app, event): app._callFn('keyPressed', app, event)
+ def keyReleased(app, event): app._callFn('keyReleased', app, event)
+ def mousePressed(app, event): app._callFn('mousePressed', app, event)
+ def mouseReleased(app, event): app._callFn('mouseReleased', app, event)
+ def mouseMoved(app, event): app._callFn('mouseMoved', app, event)
+ def mouseDragged(app, event): app._callFn('mouseDragged', app, event)
+ def timerFired(app): app._callFn('timerFired', app)
+ def sizeChanged(app): app._callFn('sizeChanged', app)
+
+####################################
+# ModalApp + Mode:
+####################################
+
+class ModalApp(App):
+ def __init__(app, activeMode=None, **kwargs):
+ app._running = False
+ app._activeMode = None
+ app.setActiveMode(activeMode)
+ super().__init__(**kwargs)
+
+ def setActiveMode(app, mode):
+ if (mode == None): mode = Mode() # default empty mode
+ if (not isinstance(mode, Mode)): raise Exception('activeMode must be a mode!')
+ if (mode.app not in [None, app]): raise Exception('Modes cannot be added to two different apps!')
+ if (app._activeMode != mode):
+ mode.app = app
+ if (app._activeMode != None): app._activeMode.modeDeactivated()
+ app._activeMode = mode
+ if (app._running): app.startActiveMode()
+
+ def startActiveMode(app):
+ app._activeMode.width, app._activeMode.height = app.width, app.height
+ if (not app._activeMode._appStartedCalled):
+ app._activeMode.appStarted() # called once per mode
+ app._activeMode._appStartedCalled = True
+ app._activeMode.modeActivated() # called each time a mode is activated
+ app._redrawAllWrapper()
+
+ def redrawAll(app, canvas):
+ if (app._activeMode != None): app._activeMode.redrawAll(canvas)
+ def appStarted(app):
+ if (app._activeMode != None): app.startActiveMode()
+ def appStopped(app):
+ if (app._activeMode != None): app._activeMode.modeDeactivated()
+ def keyPressed(app, event):
+ if (app._activeMode != None): app._activeMode.keyPressed(event)
+ def keyReleased(app, event):
+ if (app._activeMode != None): app._activeMode.keyReleased(event)
+ def mousePressed(app, event):
+ if (app._activeMode != None): app._activeMode.mousePressed(event)
+ def mouseReleased(app, event):
+ if (app._activeMode != None): app._activeMode.mouseReleased(event)
+ def mouseMoved(app, event):
+ if (app._activeMode != None): app._activeMode.mouseMoved(event)
+ def mouseDragged(app, event):
+ if (app._activeMode != None): app._activeMode.mouseDragged(event)
+ def timerFired(app):
+ if (app._activeMode != None): app._activeMode.timerFired()
+ def sizeChanged(app):
+ if (app._activeMode != None):
+ app._activeMode.width, app._activeMode.height = app.width, app.height
+ app._activeMode.sizeChanged()
+
+class Mode(App):
+ def __init__(mode, **kwargs):
+ mode.app = None
+ mode._appStartedCalled = False
+ super().__init__(autorun=False, **kwargs)
+ def modeActivated(mode): pass
+ def modeDeactivated(mode): pass
+ def loadImage(mode, path=None): return mode.app.loadImage(path)
+
+####################################
+# runApp()
+####################################
+
+'''
+def showGraphics(drawFn, **kwargs):
+ class GraphicsApp(App):
+ def __init__(app, **kwargs):
+ if ('title' not in kwargs):
+ kwargs['title'] = drawFn.__name__
+ super().__init__(**kwargs)
+ def redrawAll(app, canvas):
+ drawFn(app, canvas)
+ app = GraphicsApp(**kwargs)
+'''
+runApp = TopLevelApp
+
+print(f'Loaded cmu_112_graphics version {App.version} (last updated {App.lastUpdated})')
+
+if (__name__ == '__main__'):
+ try: import cmu_112_graphics_tests
+ except: pass
+
+
+################################################################################
+file = str(input("Please type the exact file name of the match you want to visualize. Ensure that your match data and this file are in the same directory:\n"))
+
+f = open(file)
+json_out = json.load(f)
+lines = json_out['gamedata']
+f.close()
+
+turns = len(lines)
+teams = len(lines[0]['state'])
+
+index = json_out['seat'] - 1
+
+gap = int(input('Time between frames (ms): '))
+
+#from cmu_112_graphics import *
+
+size = 47
+center = size // 2
+cs = 16 #cell size
+snap_size = 20
+
+def appStarted(app):
+ app.playing = True
+ app.timerDelay = gap
+ app.curr = 0
+ app.lastUpd = time.time()
+ app.ship_center = True
+ app.error = ''
+
+def timerFired(app):
+ # This is a Controller
+ doStep(app)
+
+def doStep(app):
+ if app.playing:
+ if app.curr < len(lines) - 1:
+ app.curr += 1
+
+ line = lines[app.curr]
+
+ app.error = ''
+ for thing in line['moves']:
+ if thing['seat'] == index + 1:
+ if 'error' in thing:
+ app.error = thing['error']
+
+ if app.error != '':
+ app.playing = False
+
+
+
+def keyPressed(app, event):
+ if event.key == 'Right' and app.curr + 1 < len(lines):
+ app.curr += 1
+ if event.key == 'Left' and app.curr > 0:
+ app.curr -= 1
+ if event.key == 'Space':
+ app.playing = not app.playing
+ if event.key == 'Tab':
+ app.ship_center = not app.ship_center
+
+def redrawAll(app, canvas):
+ line = lines[app.curr]
+
+ ships = []
+ scores = []
+ statusD = dict()
+ for state in line['state']:
+ ships.append((state[0], state[1]))
+ scores.append(state[3])
+ statusD[(state[0], state[1])] = state[2]
+
+ cx = ships[index][0]
+ cy = ships[index][1]
+
+ if app.ship_center:
+ cent_x = (snap_size * round(cx/snap_size))
+ cent_y = (snap_size * round(cy/snap_size))
+ else:
+ cent_x = cent_y = 0
+
+ for i in range(size):
+ for j in range(size):
+ x = cent_x + i - center
+ y = cent_y + (size - 1 - j) - center
+
+ if abs(x) <= 2 and abs(y) <= 2:
+ color = 'orange'
+ elif (x,y) == (cx,cy):
+ color = 'green'
+ elif (x,y) in ships:
+ color = ['red','blue','purple'][statusD[(x,y)]]
+ else:
+ color = 'white'
+ canvas.create_rectangle(cs * i + 5, cs * j + 5, cs * i + cs + 5, cs * j + cs + 5,fill=color)
+
+ if app.playing:
+ canvas.create_text(cs * size + 200, 50, text=f'Currently running',font='calibri')
+ canvas.create_text(cs * size + 200, 75, text=f'Press Space to Pause',font='calibri')
+ else:
+ canvas.create_text(cs * size + 200, 50, text=f'Currently paused',font='calibri')
+ canvas.create_text(cs * size + 200, 75, text=f'Press Space to Run',font='calibri')
+ canvas.create_text(cs * size + 200, 100, text=f'Press Left/Right to advance frame by frame',font='calibri')
+
+ canvas.create_text(cs * size + 200, 200,text=f'Frame {app.curr + 1} of {len(lines)}',font='calibri')
+ canvas.create_text(cs * size + 200, 225,text=f'Center: {(cent_x, cent_y)}',font='calibri')
+ canvas.create_text(cs * size + 200, 275,text=f'Centered near {"Ship" if app.ship_center else "Sun"}',font='calibri')
+ canvas.create_text(cs * size + 200, 300,text=f'(Tab to Switch)',font='calibri')
+
+ canvas.create_text(cs * size + 200, 400,text=f'Current Score:',font='calibri')
+ canvas.create_text(cs * size + 200, 425,text=f'{scores[index]:0.4f}',font='calibri')
+
+ if app.error:
+ canvas.create_text(cs * size + 200, 500, text=f'Program Error:',font='calibri',fill='red')
+ canvas.create_text(cs * size + 200, 525, text=f'{app.error}',font='calibri 10',fill='red')
+ canvas.create_text(cs * size + 200, 550, text=f'(autoplay stopped)',font='calibri 10',fill='red')
+
+ canvas.create_text(cs * size + 200, 600,text=f'Orange: Sun',font='calibri')
+ canvas.create_text(cs * size + 200, 625,text=f'Green: You',font='calibri')
+ canvas.create_text(cs * size + 200, 650,text=f'Red: Crashed',font='calibri')
+ canvas.create_text(cs * size + 200, 675,text=f'Blue: Counter-Clockwise',font='calibri')
+ canvas.create_text(cs * size + 200, 700,text=f'Purple: Clockwise',font='calibri')
+
+
+def main():
+ runApp(width=cs * size + 400, height=cs * size + 10)
+
+main() \ No newline at end of file