diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/game.py b/game.py
index 58712b1..f39441b 100644
--- a/game.py
+++ b/game.py
@@ -1,260 +1,306 @@
-from pair import *
-import tkinter as tk
-
-
-class Tile:
- """
-
- """
-
- def __init__(self, pos: Pair, key: str = ''):
- self.pos = pos
- self.key = tk.StringVar()
- self.key.set(key)
- self.label: tk.Label
- self.ditch = False
- self.canvas_id = None
-
-
-def weighted_choice(weights: dict):
- """
- Values in the weights dict must be of type in or float.
- Returns a key from the weights dict.
- Favors keys with greater value mappings
- """
- from random import uniform
- choice = uniform(0, sum(weights.values()))
- for key, weight in weights.items():
- if choice > weight:
- choice -= weight
- else:
- return key
-
-
-class Game:
- """
- Attributes:
- -- width : int : The length of both the grid's sides in tiles.
- -- populations : dict{str: int} : Map from all keys to their #instances in the grid.
- The sum of the values should always be width ** 2.
- -- grid : list{list{Tile}} : (0, 0) is at the top left of the screen.
- -- pos : Pair : The player's current position.
- -- targets : list{Tile} : tiles containing the target letter for a round.
- -- trail : set{Tile} : tiles the player has visited in a round.
- -- ditch : set{Tile} : tiles the player visited in the previous round.
- -- stuck : bool : Whether the player is in the ditch and has not pressed space.
- """
- LOWERCASE = {key for key in 'abcdefghijklmnopqrstuvwxyz'}
-
- def __init__(self, width: int = 20, keyset: dict = None):
- """
-
- """
- self.width = width
-
- if keyset is None:
- keyset = Game.LOWERCASE
- self.populations = {key: 0 for key in keyset}
- self.grid = [
- [Tile(Pair(x, y)) for x in range(width)]
- for y in range(width)]
-
- # initialize letters with random, balanced keys
- self.populations[''] = 0
- for row in self.grid:
- for tile in row:
- self.__shuffle_tile(tile)
- del self.populations['']
-
- self.pos = Pair(width // 2, width // 2)
- self.targets = []
- self.trail = set()
-
- # Setup the first round's targets:
- self.check_round_complete()
-
- def tile_at(self, pos: Pair):
- """
- Returns the tile at the given Pair coordinate.
- """
- if pos.in_range(self.width, self.width):
- return self.grid[pos.y][pos.x]
- else:
- return None
-
- def __adjacent(self, pos: Pair):
- """
- Returns a dict from Tile objects adjacent to
- the tile at pos to their positions as Pair objects.
- """
- adj = [(1, 0), (1, -1), (0, -1), (-1, -1),
- (-1, 0), (-1, 1), (0, 1), (1, 1)]
- adj = [pos + Pair(offset[0], offset[1]) for offset in adj]
- tile_pos = {self.tile_at(pair): pair for pair in adj}
- if None in tile_pos:
- del tile_pos[None]
- return tile_pos
-
- def move(self, key: str):
- """
- If the key parameter matches one of the adjacent
- tiles' keys, the player moves to that tile's position.
- The tile being moved out of is added to trail.
- If the user is in a position from the last round's trail,
- they must have first pressed the space-bar to move.
-
- Returns whether the player completed a round with this move.
- """
- pos = self.tile_at(self.pos)
- if pos.ditch:
- # No movement is possible when the player
- # is in a ditch that hasn't been cleared.
- pos.ditch = key == ' '
- return False
- elif key not in self.populations.keys():
- return False # Ignore keys not in the grid.
-
- # A dict from adjacent tiles to their positions:
- adj = self.__adjacent(self.pos)
- # Adjacent tiles with the same key as the key parameter:
- select = list(filter(lambda tile: tile.key.get() == key, adj))
- if select: # If an adjacent key has a matching key:
- self.trail |= {self.pos}
- # The selected tile to move to:
- select = select[0]
- self.pos = adj[select]
- if select in self.targets:
- self.targets.remove(select)
- print(self.pos, select.key.get())
- return self.check_round_complete()
- else:
- return False
-
- def __wide_adjacent(self, tile: Tile):
- """
- Return a set of keys in the 5x5 ring around tile.
- This represents keys that cannot go in tile, since
- they would create an ambiguity in movement direction.
- """
- adj = []
- for y in range(-2, 3, 1):
- adj.extend([Pair(x, y) + tile.pos for x in range(-2, 3, 1)])
- adj = {self.tile_at(pair) for pair in adj}
- if None in adj:
- adj.remove(None)
- return {t.key.get() for t in adj}
-
- def __shuffle_tile(self, tile: Tile):
- """
- Randomizes the parameter tile's key,
- favoring less-common keys in the current grid.
- """
- self.populations[tile.key.get()] -= 1
-
- lower = min(self.populations.values())
- adj = self.__wide_adjacent(tile)
- weights = { # Gives zero weight to neighboring keys.
- key: 1 / (count - lower + 1) if key not in adj else 0
- for key, count in self.populations.items()}
-
- new_key = weighted_choice(weights)
- tile.key.set(new_key)
- self.populations[new_key] += 1
-
- def check_round_complete(self):
- """
- Should be called at the end of every move.
- Returns True if the player touched the last
- target for the current round in this move.
- """
- if not self.targets:
- # The player has not yet touched
- # all tiles with the target key.
- return False
-
- # Shuffle tiles from this round's trail:
- for tile in self.trail:
- tile.key.set('')
- for tile in self.trail:
- self.__shuffle_tile(tile)
-
- # Get the new target key and
- # find tiles with matching keys:
- target = weighted_choice(self.populations)
- self.targets.clear()
- for row in self.grid:
- for tile in row:
- tile.ditch = False
- self.targets.extend(list(filter(
- lambda t: t.key.get() == target, row)
- ))
-
- # Raise ditch flags for
- # trail tiles from this round:
- for tile in self.trail:
- tile.ditch = True
- self.trail.clear()
- return True
-
-
-class SnakeyGUI(tk.Tk):
- """
-
- """
- color_schemes = {
- 'default': {
- 'bg': 'black',
- 'fg': 'white',
- 'text': 'black',
- 'pos': 'cyan',
- 'trail': 'gray80',
- 'target': 'yellow'
- }
- }
-
- def __init__(self, width: int = None):
- super(SnakeyGUI, self).__init__()
- self.title('Snakey - David Fong')
- self.game = Game() if width is None else Game(width)
- self.cs = SnakeyGUI.color_schemes['default']
-
- # Setup the grid display:
- grid = tk.Frame(self, bg='black')
- for y in range(self.game.width):
- for x in range(self.game.width):
- tile = self.game.grid[y][x]
- tile.label = tk.Label(
- grid, height=1, width=1,
- textvariable=tile.key,
- )
- tile.label.grid(row=y, column=x)
- self.grid = grid
- grid.pack()
-
- # Bind key-presses:
- self.bind('', self.move)
-
- def move(self, event):
- """
-
- """
- if self.game.move(event.keysym):
- # TODO: a round has finished. update all tile displays:
- for tile in self.game.targets:
- tile.label.onfigure(fg=self.cs['fg'])
-
- def update_cs(self, cs: str = 'default'):
- """
-
- """
- cs = SnakeyGUI.color_schemes[cs]
- self.cs = cs
- for row in self.game.grid:
- for tile in row:
- tile.label.configure(bg=cs['bg'])
-
-
-if __name__ == '__main__':
- print({None: 'hi'})
- test = SnakeyGUI()
- test.mainloop()
+from time import process_time
+
+from pair import *
+import tkinter as tk
+
+
+class Tile:
+ """
+
+ """
+
+ def __init__(self, pos: Pair, key: str = ''):
+ self.pos = pos
+ self.key = tk.StringVar()
+ self.key.set(key)
+ self.label: tk.Label
+ self.ditch = False
+ self.canvas_id = None
+
+
+def weighted_choice(weights: dict):
+ """
+ Values in the weights dict must be of type in or float.
+ Returns a key from the weights dict.
+ Favors keys with greater value mappings
+ """
+ from random import uniform
+ choice = uniform(0, sum(weights.values()))
+ for key, weight in weights.items():
+ if choice > weight:
+ choice -= weight
+ else:
+ return key
+
+
+class Game:
+ """
+ Attributes:
+ -- width : int : The length of both the grid's sides in tiles.
+ -- populations : dict{str: int} : Map from all keys to their #instances in the grid.
+ The sum of the values should always be width ** 2.
+ -- grid : list{list{Tile}} : (0, 0) is at the top left of the screen.
+ -- pos : Pair : The player's current position.
+ -- targets : list{Tile} : tiles containing the target letter for a round.
+ -- trail : set{Tile} : tiles the player has visited in a round.
+
+ -- basket : dict{str: int} : total times obtained for each letter.
+ -- start : float : process time of the start of a round.
+ """
+ LOWERCASE = {key for key in 'abcdefghijklmnopqrstuvwxyz'}
+
+ def __init__(self, width: int = 20, keyset: dict = None):
+ """
+
+ """
+ self.width = width
+
+ if keyset is None:
+ keyset = Game.LOWERCASE
+ self.populations = {key: 0 for key in keyset}
+ self.grid = [
+ [Tile(Pair(x, y)) for x in range(width)]
+ for y in range(width)]
+
+ self.pos = Pair(width // 2, width // 2)
+ self.targets = []
+ self.trail = set()
+
+ # initialize letters with random, balanced keys
+ for row in self.grid:
+ for tile in row:
+ self.__shuffle_tile(tile)
+
+ # Setup the first round's targets:
+ self.start = process_time()
+ self.check_round_complete()
+
+ def tile_at(self, pos: Pair):
+ """
+ Returns the tile at the given Pair coordinate.
+ """
+ if pos.in_range(self.width, self.width):
+ return self.grid[pos.y][pos.x]
+ else:
+ return None
+
+ def tile_at_pos(self):
+ return self.grid[self.pos.y][self.pos.x]
+
+ def __adjacent(self, pos: Pair):
+ """
+ Returns a dict from Tile objects adjacent to
+ the tile at pos to their positions as Pair objects.
+ """
+ adj = [(1, 0), (1, -1), (0, -1), (-1, -1),
+ (-1, 0), (-1, 1), (0, 1), (1, 1)]
+ adj = [pos + Pair(offset[0], offset[1]) for offset in adj]
+ tile_pos = {self.tile_at(pair): pair for pair in adj}
+ if None in tile_pos:
+ del tile_pos[None]
+ return tile_pos
+
+ def move(self, key: str):
+ """
+ If the key parameter matches one of the adjacent
+ tiles' keys, the player moves to that tile's position.
+ The tile being moved out of is added to trail.
+ If the user is in a position from the last round's trail,
+ they must have first pressed the space-bar to move.
+
+ Returns whether the player completed a round with this move.
+ """
+ pos = self.tile_at_pos()
+ if pos.ditch:
+ # No movement is possible when the player
+ # is in a ditch that hasn't been cleared.
+ pos.ditch = key == ' '
+ return False
+ elif key not in self.populations.keys():
+ return False # Ignore keys not in the grid.
+
+ # A dict from adjacent tiles to their positions:
+ adj = self.__adjacent(self.pos)
+ # Adjacent tiles with the same key as the key parameter:
+ select = list(filter(lambda tile: tile.key.get() == key, adj))
+ if select: # If an adjacent key has a matching key:
+ self.trail |= {self.tile_at_pos()}
+ # The selected tile to move to:
+ select = select[0]
+ self.pos = adj[select]
+ if select in self.targets:
+ self.targets.remove(select)
+ # debug: print(self.pos, select.key.get())
+ return self.check_round_complete()
+ else:
+ return False
+
+ def __wide_adjacent(self, tile: Tile):
+ """
+ Return a set of keys in the 5x5 ring around tile.
+ This represents keys that cannot go in tile, since
+ they would create an ambiguity in movement direction.
+ """
+ adj = []
+ for y in range(-2, 3, 1):
+ adj.extend([Pair(x, y) + tile.pos for x in range(-2, 3, 1)])
+ del adj[12] # The current position.
+ adj = {self.tile_at(pair) for pair in adj}
+ if None in adj:
+ adj.remove(None)
+ return {t.key.get() for t in adj}
+
+ def __shuffle_tile(self, tile: Tile):
+ """
+ Randomizes the parameter tile's key,
+ favoring less-common keys in the current grid.
+ """
+ lower = min(self.populations.values())
+ adj = self.__wide_adjacent(tile)
+ weights = { # Gives zero weight to neighboring keys.
+ key: 1 / (count - lower + 1) if key not in adj else 0
+ for key, count in self.populations.items()}
+
+ new_key = weighted_choice(weights)
+ tile.key.set(new_key)
+ self.populations[new_key] += 1
+
+ def check_round_complete(self):
+ """
+ Should be called at the end of every move.
+ Returns True if the player touched the last
+ target for the current round in this move.
+ """
+ if self.targets:
+ # The player has not yet touched
+ # all tiles with the target key.
+ return False
+
+ now = process_time()
+ elapsed = now - self.start
+ self.start = now
+
+ # Shuffle tiles from this round's trail:
+ for tile in self.trail:
+ self.populations[tile.key.get()] -= 1
+ tile.key.set('')
+ for tile in self.trail:
+ self.__shuffle_tile(tile)
+
+ # Get the new target key and
+ # find tiles with matching keys:
+ target = weighted_choice(self.populations)
+ self.targets.clear()
+ for row in self.grid:
+ for tile in row:
+ tile.ditch = False
+ self.targets.extend(list(filter(
+ lambda t: t.key.get() == target, row)
+ ))
+ # debug: self.targets = [self.targets[0], ]
+
+ # Raise ditch flags for
+ # trail tiles from this round:
+ for tile in self.trail:
+ tile.ditch = True
+ self.trail.clear()
+ return True
+
+
+class SnaKeyGUI(tk.Tk):
+ """
+ Attributes:
+ -- game : Game
+ -- cs : dict{str: dict{str: str}}
+ -- grid: : Frame
+ """
+ color_schemes = {
+ 'default': {
+ 'bg': 'black',
+ 'fg': 'white',
+ 'text': 'black',
+ 'pos': 'cyan',
+ 'targets': 'yellow',
+ 'trail': 'gray80',
+ 'ditch': 'gray50',
+ 'ditch_targets': 'orange',
+ }
+ }
+
+ def __init__(self, width: int = None):
+ super(SnaKeyGUI, self).__init__()
+ self.title('Snakey - David Fong')
+ self.game = Game() if width is None else Game(width)
+
+ # Setup the grid display:
+ grid = tk.Frame(self, bg='black')
+ for y in range(self.game.width):
+ for x in range(self.game.width):
+ tile = self.game.grid[y][x]
+ tile.label = tk.Label(
+ grid, height=1, width=1,
+ textvariable=tile.key,
+ )
+ tile.label.grid(row=y, column=x)
+ self.grid = grid
+ grid.pack()
+
+ # Setup the colors:
+ self.cs = SnaKeyGUI.color_schemes['default']
+ self.update_cs()
+
+ # Bind key-presses:
+ self.bind('', self.move)
+
+ def move(self, event):
+ """
+
+ """
+ self.game.tile_at_pos().label.configure(bg=self.cs['trail'])
+
+ # Execute the move in the internal representation
+ # and check if the move resulted in the round ending:
+ if self.game.move(event.keysym):
+ # If round over, update entire display.
+ self.update_cs()
+
+ # Highlight new position tile:
+ self.game.tile_at_pos().label.configure(bg=self.cs['pos'])
+
+ def update_cs(self, cs: str = None):
+ """
+
+ """
+ if cs is None:
+ cs = self.cs
+ else:
+ self.cs = cs
+ cs = SnaKeyGUI.color_schemes[cs]
+
+ # Recolor all tiles:
+ for row in self.game.grid:
+ for tile in row:
+ tile.label.configure(bg=cs['fg'], fg=cs['text'])
+ if tile.ditch:
+ tile.label.configure(bg=cs['ditch'])
+
+ # Highlight the player's current position:
+ self.game.tile_at_pos().label\
+ .configure(bg=cs['pos'])
+
+ # Highlight tiles from the player's trail:
+ for tile in self.game.trail:
+ tile.label.configure(bg=cs['trail'])
+
+ # Highlight tiles that need to be touched
+ # to complete the current round:
+ for tile in self.game.targets:
+ tile.label.configure(bg=cs['targets'])
+ if tile.ditch:
+ tile.label.configure(bg=cs['ditch_targets'])
+
+
+if __name__ == '__main__':
+ print({None: 'hi'})
+ test = SnaKeyGUI()
+ test.mainloop()