#!/usr/bin/env python3 # Copyright © 2012-13 Qtrac Ltd. All rights reserved. # This program or module is free software: you can redistribute it # and/or modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. It is provided for # educational purposes and is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. import collections import heapq import math import random import tkinter as tk import tkinter.messagebox as messagebox from Globals import * # Need to allow for them to be darkened/lightened for 3D shadow. COLORS = [ "#7F0000", # Red "#007F00", # Green "#00007F", # Blue "#007F7F", # Cyan "#7F007F", # Magenta "#7F7F00", # Yellow "#A0A0A4", # Gray "#A52A2A", # Brown ] DEF_COLUMNS = 9 MIN_COLUMNS = 5 MAX_COLUMNS = 30 DEF_ROWS = 9 MIN_ROWS = 5 MAX_ROWS = 30 DEF_MAX_COLORS = 4 MIN_MAX_COLORS = 2 MAX_MAX_COLORS = len(COLORS) class Board(tk.Canvas): def __init__(self, master, set_status_text, scoreText, columns=DEF_COLUMNS, rows=DEF_ROWS, maxColors=DEF_MAX_COLORS, delay=500, size=40, outline="#DFDFDF"): self.columns = columns self.rows = rows self.maxColors = maxColors self.delay = delay self.outline = outline self.size = size self.set_status_text = set_status_text self.scoreText = scoreText self.score = 0 self.highScore = 0 super().__init__(master, width=self.columns * self.size, height=self.rows * self.size) self.pack(fill=tk.BOTH, expand=True) self.bind("<ButtonRelease>", self._click) self.new_game() def new_game(self, event=None): self.score = 0 random.shuffle(COLORS) colors = COLORS[:self.maxColors] self.tiles = [] for x in range(self.columns): self.tiles.append([]) for y in range(self.rows): self.tiles[x].append(random.choice(colors)) self._draw() self.update_score() def _draw(self, *args): self.delete("all") self.config(width=self.columns * self.size, height=self.rows * self.size) for x in range(self.columns): x0 = x * self.size x1 = x0 + self.size for y in range(self.rows): y0 = y * self.size y1 = y0 + self.size self._draw_square(self.size, x0, y0, x1, y1, self.tiles[x][y], self.outline) self.update() # |\__t__/| # |l| m |r| # |/-----\| # ----b---- # def _draw_square(self, size, x0, y0, x1, y1, color, outline): if color is None: light, color, dark = (outline,) * 3 else: light, color, dark = self._three_colors(color) offset = 4 self.create_polygon( # top x0, y0, x0 + offset, y0 + offset, x1 - offset, y0 + offset, x1, y0, fill=light, outline=light) self.create_polygon( # left x0, y0, x0, y1, x0 + offset, y1 - offset, x0 + offset, y0 + offset, fill=light, outline=light) self.create_polygon( # right x1 - offset, y0 + offset, x1, y0, x1, y1, x1 - offset, y1 - offset, fill=dark, outline=dark) self.create_polygon( # bottom x0, y1, x0 + offset, y1 - offset, x1 - offset, y1 - offset, x1, y1, fill=dark, outline=dark) self.create_rectangle( # middle x0 + offset, y0 + offset, x1 - offset, y1 - offset, fill=color, outline=color) def _three_colors(self, color): r, g, b = self.winfo_rgb(color) color = "#{:04X}{:04X}{:04X}".format(r, g, b) dark = "#{:04X}{:04X}{:04X}".format(max(0, int(r * 0.5)), max(0, int(g * 0.5)), max(0, int(b * 0.5))) light = "#{:04X}{:04X}{:04X}".format(min(0xFFFF, int(r * 1.5)), min(0xFFFF, int(g * 1.5)), min(0xFFFF, int(b * 1.5))) return light, color, dark def _click(self, event): x = event.x // self.size y = event.y // self.size color = self.tiles[x][y] if color is None or not self._is_legal(x, y, color): return self._dim_adjoining(x, y, color) def _is_legal(self, x, y, color): """A legal click is on a colored tile that is adjacent to another tile of the same color.""" if x > 0 and self.tiles[x - 1][y] == color: return True if x + 1 < self.columns and self.tiles[x + 1][y] == color: return True if y > 0 and self.tiles[x][y - 1] == color: return True if y + 1 < self.rows and self.tiles[x][y + 1] == color: return True return False def _dim_adjoining(self, x, y, color): adjoining = set() self._populate_adjoining(x, y, color, adjoining) self.score += len(adjoining) ** (self.maxColors - 2) for x, y in adjoining: self.tiles[x][y] = "#F0F0F0" self._draw() self.after(self.delay, lambda: self._delete_adjoining(adjoining)) def _populate_adjoining(self, x, y, color, adjoining): if not ((0 <= x < self.columns) and (0 <= y < self.rows)): return # Fallen off an edge if (x, y) in adjoining or self.tiles[x][y] != color: return # Color doesn't match or already done adjoining.add((x, y)) self._populate_adjoining(x - 1, y, color, adjoining) self._populate_adjoining(x + 1, y, color, adjoining) self._populate_adjoining(x, y - 1, color, adjoining) self._populate_adjoining(x, y + 1, color, adjoining) def _delete_adjoining(self, adjoining): for x, y in adjoining: self.tiles[x][y] = None self._draw() self.after(self.delay, self._close_up) def _close_up(self): self._move() self._draw() self._check_game_over() def _move(self): moved = True while moved: moved = False for x in range(self.columns): for y in range(self.rows): if self.tiles[x][y] is not None: if self._move_if_possible(x, y): moved = True break def _move_if_possible(self, x, y): empty_neighbours = self._empty_neighbours(x, y) if empty_neighbours: move, nx, ny = self._nearest_to_middle(x, y, empty_neighbours) if move: self.tiles[nx][ny] = self.tiles[x][y] self.tiles[x][y] = None return True return False def _empty_neighbours(self, x, y): neighbours = set() for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)): if (0 <= nx < self.columns and 0 <= ny < self.rows and self.tiles[nx][ny] is None): neighbours.add((nx, ny)) return neighbours def _nearest_to_middle(self, x, y, empty_neighbours): color = self.tiles[x][y] midX = self.columns // 2 midY = self.rows // 2 Δold = math.hypot(midX - x, midY - y) heap = [] for nx, ny in empty_neighbours: if self._is_square(nx, ny): Δnew = math.hypot(midX - nx, midY - ny) if self._is_legal(nx, ny, color): Δnew -= 0.1 # Make same colors slightly attractive heapq.heappush(heap, (Δnew, nx, ny)) Δnew, nx, ny = heap[0] return (True, nx, ny) if Δold > Δnew else (False, x, y) def _is_square(self, x, y): if x > 0 and self.tiles[x - 1][y] is not None: return True if x + 1 < self.columns and self.tiles[x + 1][y] is not None: return True if y > 0 and self.tiles[x][y - 1] is not None: return True if y + 1 < self.rows and self.tiles[x][y + 1] is not None: return True return False def _check_game_over(self): userWon, canMove = self._check_tiles() title = message = None if userWon: title, message = self._user_won() elif not canMove: title = "Game Over" message = "Game over with a score of {:,}.".format( self.score) if title is not None: messagebox.showinfo("{} — {}".format(title, APPNAME), message, parent=self) self.new_game() else: self.update_score() def _check_tiles(self): countForColor = collections.defaultdict(int) userWon = True canMove = False for x in range(self.columns): for y in range(self.rows): color = self.tiles[x][y] if color is not None: countForColor[color] += 1 userWon = False if self._is_legal(x, y, color): # We _can_ move canMove = True if 1 in countForColor.values(): canMove = False return userWon, canMove def _user_won(self): title = "Winner!" message = "You won with a score of {:,}.".format(self.score) if self.score > self.highScore: self.highScore = self.score message += "\nThat's a new high score!" return title, message def update_score(self): self.scoreText.set("{:,} ({:,})".format(self.score, self.highScore)) if __name__ == "__main__": import sys if sys.stdout.isatty(): application = tk.Tk() application.title("Board") scoreText = tk.StringVar() board = Board(application, print, scoreText) application.mainloop() else: print("Loaded OK")