# Minesweeper game for iOS in Python (requires Pythonista 2)
# Author: <NAME> <<EMAIL>>
# Sprites: http://www.curtisbright.com/msx/

from PIL import Image
from StringIO import *
from base64 import b64decode

from datetime import datetime
from collections import namedtuple

from scene import *
from random import shuffle
from functools import partial

Sprite = namedtuple('Sprite',('name','size'))

class State:
	thinking, playing, lost, won = range(4)

class Game(Scene):
		
	def setup(self):
		self.width = 8
		self.height = 8
		self.bombs = 10
		self.holding = 15
		self.scale = self.calculate_scale()
		self.sprites = self.load_sprites()
		self.layers = self.add_layers()
		self.new_game()

	def calculate_scale(self):
		h_scale = int((self.size.w/(self.width))/16)
		v_scale = int((self.size.h/(self.height+3))/16)
		return min(h_scale,v_scale)

	def load_sprite(self,image,bounds):
		width = bounds[2]-bounds[0]
		height = bounds[3]-bounds[1]
		size = Size(width*self.scale,height*self.scale)
		name = load_pil_image(image.crop(bounds).resize((int(size.w),int(size.h))))
		return Sprite(name,size)
		
	def load_bg_sprite(self,image):
		w = self.width
		h = self.height
		size = Size(w*16+12*2, h*16+11*3+33)
		background = Image.new('RGBA',(int(size.w),int(size.h)),"silver")
		b = [([0,82,12,93],[0,0,12,11]),
				 ([13,82,14,93],[12,0,12+w*16,11]),
				 ([15,82,27,93],[12+w*16,0,12+w*16+12,11]),
				 ([0,94,12,95],[0,11,12,11+33]),
				 ([15,94,27,95],[12+w*16,11,12+w*16+12,11+33]),
				 ([0,96,12,107],[0,11+33,12,11+33+11]),
				 ([13,96,14,107],[12,11+33,12+w*16,11+33+11]),
				 ([15,96,27,107],[12+w*16,11+33,12+w*16+12,11+33+11]),
				 ([0,108,12,109],[0,11+33+11,12,11+33+11+h*16]),
				 ([15,108,27,109],[12+w*16,11+33+11,12+w*16+12,11+33+11+h*16]),
				 ([0,110,12,121],[0,11+33+11+h*16,12,11+33+11+h*16+11]),
				 ([13,110,14,121],[12,11+33+11+h*16,12+w*16,11+33+11+h*16+11]),
				 ([15,110,27,121],[12+w*16,11+33+11+h*16,12+w*16+12,11+33+11+h*16+11]),
				 ([28,82,69,107],[12+4,11+4,12+4+41,11+4+25]),
				 ([28,82,69,107],[12+w*16-4-41,11+4,12+w*16-4,11+4+25])]
		for (s,t) in b:
			background.paste(image.crop(s).resize((t[2]-t[0],t[3]-t[1])),t)
		size = Size(size.w * self.scale, size.h * self.scale)
		background = background.resize((int(size.w),int(size.h)))
		name = load_pil_image(background)
		return Sprite(name,size)

	def load_sprites(self):
		data ='''
		iVBORw0KGgoAAAANSUhEUgAAAJAAAAB6CAMAAABnRypuAAADAFBMVEUAAACA
		AAAAgACAgAAAAICAAIAAgIDAwMCAgID/AAAA/wD//wAAAP//AP8A//////8Q
		EBARERESEhITExMUFBQVFRUWFhYXFxcYGBgZGRkaGhobGxscHBwdHR0eHh4f
		Hx8gICAhISEiIiIjIyMkJCQlJSUmJiYnJycoKCgpKSkqKiorKyssLCwtLS0u
		Li4vLy8wMDAxMTEyMjIzMzM0NDQ1NTU2NjY3Nzc4ODg5OTk6Ojo7Ozs8PDw9
		PT0+Pj4/Pz9AQEBBQUFCQkJDQ0NERERFRUVGRkZHR0dISEhJSUlKSkpLS0tM
		TExNTU1OTk5PT09QUFBRUVFSUlJTU1NUVFRVVVVWVlZXV1dYWFhZWVlaWlpb
		W1tcXFxdXV1eXl5fX19gYGBhYWFiYmJjY2NkZGRlZWVmZmZnZ2doaGhpaWlq
		ampra2tsbGxtbW1ubm5vb29wcHBxcXFycnJzc3N0dHR1dXV2dnZ3d3d4eHh5
		eXl6enp7e3t8fHx9fX1+fn5/f3+AgICBgYGCgoKDg4OEhISFhYWGhoaHh4eI
		iIiJiYmKioqLi4uMjIyNjY2Ojo6Pj4+QkJCRkZGSkpKTk5OUlJSVlZWWlpaX
		l5eYmJiZmZmampqbm5ucnJydnZ2enp6fn5+goKChoaGioqKjo6OkpKSlpaWm
		pqanp6eoqKipqamqqqqrq6usrKytra2urq6vr6+wsLCxsbGysrKzs7O0tLS1
		tbW2tra3t7e4uLi5ubm6urq7u7u8vLy9vb2+vr6/v7/AwMDBwcHCwsLDw8PE
		xMTFxcXGxsbHx8fIyMjJycnKysrLy8vMzMzNzc3Ozs7Pz8/Q0NDR0dHS0tLT
		09PU1NTV1dXW1tbX19fY2NjZ2dna2trb29vc3Nzd3d3e3t7f39/g4ODh4eHi
		4uLj4+Pk5OTl5eXm5ubn5+fo6Ojp6enq6urr6+vs7Ozt7e3u7u7v7+/w8PDx
		8fHy8vLz8/P09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7///9Q
		<KEY>
		<KEY>
		IF2pDeQn+xJ5njeAvFaCdOVEIPLiDdKVUyPkX01CunJyhLBfSFfuCLWJkCEI
		2037CCFQPzeO0JaybR6yL2f7mUfo/Ey9pWwcoR73squssn5WgO48dGfqT4Eu
		GaGLnYdaZmplc215ptY31yL5JhDby8T/KklLTaCKvqoIj9A4imoDoCa9bgg/
		D80XiNDW8aMtkHL8aAwUZ+ob6Aa6geoAhZe1I/+LmkZA5OXxJYDoy+wrAP0D
		7BQA5IVg6IYAAAAASUVORK5CYII=
		'''
		image = Image.open(StringIO(b64decode(data))).convert('RGBA')
		f = partial(self.load_sprite,image)
		sprites = {}
		sprites['numbers'] = map(lambda x: f([x*16,0,x*16+16,16]),xrange(9))
		sprites['icons'] = map(lambda x: f([x*16,16,x*16+16,32]),xrange(8))
		sprites['digits'] = map(lambda x: f([x*12,33,x*12+11,54]),xrange(11))
		sprites['buttons'] = map(lambda x: f([x*27,55,x*27+26,81]),xrange(5))
		sprites['background'] = self.load_bg_sprite(image)
		return namedtuple('Sprites',sprites.keys())(*sprites.values())

	def add_layers(self):
		# root layer
		self.root_layer = Layer(self.bounds)
		# background layer
		width = self.sprites.background.size.w
		height = self.sprites.background.size.h
		offset = Point((self.size.w - width)/2,(self.size.h - height)/2)
		bg = Layer(Rect(offset.x,offset.y,width,height))
		bg.image = self.sprites.background.name
		self.add_layer(bg)
		# help variables
		bg_left = offset.x
		bg_right = offset.x+width
		bg_top = offset.y+height
		bg_bottom = offset.y
		# smiley button layer
		width = self.sprites.buttons[0].size.w
		height = self.sprites.buttons[0].size.h
		offset_x = (self.size.w - width)/2
		offset_y = bg_top-(11+4)*self.scale-height
		button = Layer(Rect(offset_x,offset_y,width,height))
		button.image = self.sprites.buttons[0].name
		self.add_layer(button)
		# bomb count layer
		width = self.sprites.digits[0].size.w
		height = self.sprites.digits[0].size.h
		offset_x = bg_left+(12+4+2)*self.scale
		offset_y = bg_top-(11+4+2)*self.scale-height
		bombs = []
		for i in xrange(3):
			x = offset_x+i*(width+2*self.scale)
			count = Layer(Rect(x,offset_y,width,height))
			count.image = self.sprites.digits[0].name
			self.add_layer(count)
			bombs.append(count)
		# time count layer
		width = self.sprites.digits[0].size.w
		height = self.sprites.digits[0].size.h
		offset_x = bg_right-(12+4+2*3)*self.scale-width*3
		offset_y = bg_top-(11+4+2)*self.scale-height
		seconds = []
		for i in xrange(3):
			x = offset_x+i*(width+2*self.scale)
			count = Layer(Rect(x,offset_y,width,height))
			count.image = self.sprites.digits[0].name
			self.add_layer(count)
			seconds.append(count)
		# tile layers
		tile_width = self.sprites.icons[0].size.w
		tile_height = self.sprites.icons[0].size.h
		width = self.width * tile_width
		height = self.height * tile_height
		offset_x = (self.size.w - width)/2
		offset_y = bg_bottom+11*self.scale
		tiles = []
		for i in xrange(self.width*self.height):
			x, y = i % self.width, i / self.width
			x, y = offset_x + x * tile_width, offset_y + y * tile_height
			tile = Layer(Rect(x, y, tile_width, tile_height))
			self.add_layer(tile)
			tiles.append(tile)
		layers = {}
		layers['root'] = self.root_layer
		layers['background'] = bg
		layers['button'] = button
		layers['bombs'] = bombs
		layers['seconds'] = seconds
		layers['tiles'] = tiles
		return namedtuple('Layers',layers.keys())(*layers.values())

	def draw(self):
		background(.8,.8,.8)
		self.layers.root.draw()
		self.draw_button()
		self.draw_counts()
		self.draw_tiles()
		
	def draw_counts(self):
		if self.state in [State.thinking, State.playing]:
			bombs_left = self.bombs_left()
			if bombs_left<0:
				self.layers.bombs[0].image = self.sprites.digits[10].name
				for i in xrange(2):
					digit = (abs(bombs_left)/pow(10,i))%10
					self.layers.bombs[2-i].image = self.sprites.digits[digit].name
			else:
				for i in xrange(3):
					digit = (bombs_left/pow(10,i))%10
					self.layers.bombs[2-i].image = self.sprites.digits[digit].name
			if self.start == None:
				seconds_passed = 0
			else:
				seconds_passed = int((datetime.now() - self.start).total_seconds())
			for i in xrange(3):
				digit = (seconds_passed/pow(10,i))%10
				self.layers.seconds[2-i].image = self.sprites.digits[digit].name

	def draw_tiles(self):
		for tile in self.layers.tiles:
			self.draw_tile(tile)
			
	def bombs_left(self):
		marked = 0
		moves = 0
		for tile in self.layers.tiles:
			if tile.marked:
				marked += 1
			if tile.bomb:
				if tile.open:
					self.state = State.lost
			else:
				if not tile.open:
					moves += 1
		if moves == 0:
			self.state = State.won
			return 0
		return self.bombs - marked

	def draw_button(self):
		if self.state in [State.thinking, State.playing]:
			self.state = State.thinking
			for tile in self.layers.tiles:
				if not tile.open and tile.selected:
					self.state = State.playing
					break
		if self.layers.button.selected:
			self.layers.button.image = self.sprites.buttons[4].name
		else:
			self.layers.button.image = self.sprites.buttons[self.state].name

	def new_game(self):
		self.state = State.thinking
		self.layers.button.selected = False
		self.start = None
		bomb_locations = [True] * self.bombs
		bomb_locations+= [False] * (self.width * self.height - self.bombs)
		shuffle(bomb_locations)
		for i in xrange(len(self.layers.tiles)):
			x, y = i % self.width, i / self.width
			tile = self.layers.tiles[i]
			tile.bomb = bomb_locations[i]
			tile.selected = False
			tile.hold = 0
			tile.marked = False
			tile.open = False
			tile.first = False
			#tile.image = self.sprites.icons[0].name
			tile.score = 0
			for neighbour in self.get_neighbours(tile):
				if bomb_locations[self.layers.tiles.index(neighbour)]:
					tile.score += 1

	def get_neighbours(self,tile):
		i = self.layers.tiles.index(tile)
		x, y = i % self.width, i / self.width
		neighbours = ((-1,-1),(0,-1),(1,-1),(-1,0),(1,0),(-1,1),(0,1),(1,1))
		for (dx,dy) in neighbours:
			if x+dx>=0 and x+dx<self.width and y+dy>=0 and y+dy<self.height:
				n = (y+dy)*self.width+x+dx
				yield self.layers.tiles[n]
						
	def draw_tile(self,tile):
		if tile.selected:
			tile.hold += 1
		else:
			tile.hold = 0
		if tile.open:
			if tile.bomb:
				tile.image = self.sprites.icons[5].name
			else:
				tile.image = self.sprites.numbers[tile.score].name
			if tile.hold == self.holding:
				self.open_neighbours(tile)
		else:
			if self.state in [State.thinking, State.playing]:
				if tile.hold == self.holding:
					tile.marked = not tile.marked
				if tile.marked:
					tile.image = self.sprites.icons[3].name
				elif tile.selected:
					tile.image = self.sprites.icons[1].name
				else:
					tile.image = self.sprites.icons[0].name
			if self.state == State.lost:
				if tile.bomb and not tile.marked:
					tile.image = self.sprites.icons[2].name
				if not tile.bomb and tile.marked:
					tile.image = self.sprites.icons[4].name
			if self.state == State.won:
				if tile.bomb:
					tile.image = self.sprites.icons[3].name

	def touch_tile(self, touch, release):
		for tile in self.layers.tiles:
			tile.selected = touch.location in tile.frame
			if tile.selected and release:
				tile.selected = False
				if not tile.marked and tile.hold < self.holding:
					if self.start == None and tile.bomb:
						self.new_game()
						return self.touch_tile(touch,True)
					self.open_tile(tile)
				if self.start == None:
					self.start = datetime.now()

	def open_tile(self,tile):
		tile.open = True
		if tile.score == 0 and not tile.bomb:
			for neighbour in self.get_neighbours(tile):
				if not neighbour.open:
					self.open_tile(neighbour)
	
	def open_neighbours(self,tile):
		for neighbour in self.get_neighbours(tile):
			if not neighbour.marked:
				neighbour.open = True
			if neighbour.score == 0 and not neighbour.bomb:
				self.open_tile(neighbour)
	
	def touch_button(self, touch, release):
		button = self.layers.button
		button.selected = touch.location in button.frame
		if button.selected and release:
			self.new_game()
	
	def touch_began(self, touch):
		if self.state in [State.thinking, State.playing]:
			self.touch_tile(touch, False)
		self.touch_button(touch, False)
		
	def touch_moved(self,touch):
		self.touch_began(touch)

	def touch_ended(self, touch):
		if self.state in [State.thinking, State.playing]:
			self.touch_tile(touch, True)
		self.touch_button(touch, True)
		
run(Game(),PORTRAIT)
