示例#1
0
	def __init__(self):
		self._master = Tk()
	
		# frame
		frame = Frame(self._master)
		#frame.bind("<Key>", self.key_pressed)
		# keys to end game
		frame.bind("<Escape>", self.end)
		frame.bind("<Return>", self.end)
		frame.bind("<space>", self.end)
		frame.focus_set() # this is very important
		frame.grid()
		frame.master.title("Settlers of Catan: W****s and Wenches (custom expansion)")
		frame.pack()
		
		# keeps index of turn
		self._turn = 0
		
		# now to get a grid
		for i in range(6):
			frame.master.columnconfigure(i, weight=1)
		frame.master.rowconfigure(0, weight=1)

		frame1 = frame
		self._frame = frame

		# canvas
		w = Canvas(frame1, width=CatanApp.width, height=CatanApp.height)
		w.pack()
		
		# create the board
		self._map = MapGen()
		self._map.create_players(self.players)
		self._map.gen()
		self.set_vertices(self._map.get_map())
		self._map.prepare()
		
		self.draw_board(w)
		
		#self.play_game()
		self.draw_status_area()
		self._roll_var = StringVar()
		
		self._roll_button = Button(
			frame1, 
			text="End turn", 
			command=self.roll,
			state=DISABLED # initially hide it
		)
		#self._roll_button.place(x=600, y=20)
		#self._roll_button.pack()
		
		self._canvas.create_window(60, self.height + 10, window=self._roll_button, anchor=S)
		
		l = Label(frame1, textvar=self._roll_var)
		l.place(x=600, y=120)
		l.pack()
		
		self.create_build_buttons()
		
		self._canvas.create_window(170, self.height + 10, window=self._build_buttons["settlement"], anchor=S)
		
		self.draw_robber()
		# bind events to the robber
		self._canvas.tag_bind("robber", "<ButtonPress-1>", self.grab_robber)
		self._canvas.tag_bind("robber", "<B1-Motion>", self.move_robber)
		self._canvas.tag_bind("robber", "<ButtonRelease-1>", self.release_robber)
		
		
		self.automate_setup()
示例#2
0
class CatanApp():
	'''The Catan App (for now).
	For now side distances are determined statically. Can dynamically allocate them at a later point.'''
	
	height = 550
	width = 900
	minor_horiz_dist = 30
	major_horiz_dist = 60
	minor_vert_dist = 50
	render_start = (200, minor_vert_dist + 20)
	
	settlement_radius = 20
	city_radius = 22
	
	robber_height = 40
	robber_width = 40
	
	players = ["red", "gold2", "blue", "green"]
	
	def draw_robber(self):
		# the robber has an associated hex
		center = self._map.get_robber_hex().get_center()
		self._robber_sprite = self._canvas.create_oval(
			center[0] - self.robber_width / 2,
			center[1] - self.robber_height / 2,
			center[0] + self.robber_width / 2,
			center[1] + self.robber_height / 2,
			tag="robber",
			fill="black",
			activefill="yellow",
			state=DISABLED
		)
		
	def grab_robber(self, event):
		self._grab_item = {
			"x" : event.x,
			"y" : event.y,
			"item" : self._canvas.find_withtag("robber")
		}
		
	def move_robber(self, event):
		self._canvas.move(
			self._grab_item["item"],
			event.x - self._grab_item["x"], 
			event.y - self._grab_item["y"]
		)
		self._grab_item["x"] = event.x
		self._grab_item["y"] = event.y
		
	def release_robber(self, event):
		# get the closest hex...
		items = self._canvas.find_overlapping(event.x - 15, event.y - 15, event.x + 15, event.y + 15)
		
		for item in items:
			tags = self._canvas.gettags(item)
			if "hex" in tags:
				coord = tuple([int(i) for i in tags[1].replace("hex_", "").split("_")])
				
				if self._map.set_robber_hex(coord[0], coord[1]):
					center = self._map.get_robber_hex().get_center()
					self._canvas.move(
						self._robber_sprite,
						center[0] - self._grab_item["x"],
						center[1] - self._grab_item["y"]
					)
					
					# TODO - allow me to re-pickup the robber and move it again
					self.enable_stealing()
				else:
					print "ROBBER FAIL"
					self.move_robber_to_hex(self._map.get_robber_hex())
					return
			
	def enable_stealing(self):
		'''Enable stealing by the current player from all players adjoining the robber hex.'''
		
		# only allow stealing from players who are adjacent to that hex
		player_list = self._map.get_players_on_robber_hex()
		
		if len(player_list) == 0:
			# don't steal from anybody
			self.change_to_state("gameplay")
		elif len(player_list) == 1:
			# don't go through trouble of selection here, since there is only 1 choice
			self.steal_from_player(player_list[0])
			self.change_to_state("gameplay")
		else:
			for c in player_list:
				t = "player_hand_rect_%s" % c
				self._canvas.itemconfigure(self._canvas.find_withtag(t)[0], state=NORMAL)
			
	
	def _next_turn(self, decr=False):
		i=-1 if decr else 1
		self._turn = (self._turn + i) % 4
		
	def _get_turn(self):
		return self._turn
	
	def move_robber_to_hex(self, destination_hex):
		'''Move robber to the given hex. Here, hex is the Hex object.'''
		
		new_pos = destination_hex.get_center()
		old_pos = self._canvas.coords(self._robber_sprite)
		old_pos_c = ((old_pos[0] + old_pos[2]) / 2, (old_pos[1] + old_pos[3]) / 2)
		self._canvas.move(self._robber_sprite, new_pos[0] - old_pos_c[0], new_pos[1] - old_pos_c[1])
	
	def automate_robber(self):
		'''Automate the robber completely.'''
		
		self.automate_robber_movement()
		self.automate_robber_steal_picking()
		#self.change_to_state("gameplay")
	
	def automate_robber_steal_picking(self):
		'''Automate the process of picking a person to steal from, once robber has been placed.'''
		
		self.steal_from_player(self._map.ai.get_random_robber_pick())
	
	def automate_robber_movement(self):
		'''Choose a place to put the robber.'''
		
		good_placement = False
		
		color = self.players[self._turn]
		row, col = self._map.ai.get_smart_robber_placement(color)
		#destination_hex_sprite = self._canvas.find_withtag("hex_{}_{}".format(row, col))
		model_hex = self._map._board[row][col]
		
		if self._map.set_robber_hex(row, col):
			self.move_robber_to_hex(model_hex)
		else:
			print "AI picked same hex to put robber, skipping stealing..."
			#self.move_robber_to_hex(self._map.get_robber_hex())
	
	def automate_setup(self):
		'''Create a random setup of the first two roads and settlements.'''
		
		# do all 8
		for i in range(8):
			c = self.players[self._turn]
			v = self._map.ai.get_best_settlement()
			self.add_settlement(None, v)
			road = self._map.ai.get_random_road_from_settlement(v)
			
			if not road:
				print "ERROR! Could not find a place to put a road"
			else:
				v1, v2 = road
			
			if v1[0] > v2[0]:
				v1, v2 = v2, v1 # switch places to correspond with the way roads are indexed
			
#			if (v1, v2) not in self._roads:
#				print "Tried to build road from {} to {}".format(v1, v2)
#				for k in self._roads.keys():
#					if k[0] == v1 or k[1] == v1:
#						print k
			
			self.add_road(None, v1, v2)
	
	def __init__(self):
		self._master = Tk()
	
		# frame
		frame = Frame(self._master)
		#frame.bind("<Key>", self.key_pressed)
		# keys to end game
		frame.bind("<Escape>", self.end)
		frame.bind("<Return>", self.end)
		frame.bind("<space>", self.end)
		frame.focus_set() # this is very important
		frame.grid()
		frame.master.title("Settlers of Catan: W****s and Wenches (custom expansion)")
		frame.pack()
		
		# keeps index of turn
		self._turn = 0
		
		# now to get a grid
		for i in range(6):
			frame.master.columnconfigure(i, weight=1)
		frame.master.rowconfigure(0, weight=1)

		frame1 = frame
		self._frame = frame

		# canvas
		w = Canvas(frame1, width=CatanApp.width, height=CatanApp.height)
		w.pack()
		
		# create the board
		self._map = MapGen()
		self._map.create_players(self.players)
		self._map.gen()
		self.set_vertices(self._map.get_map())
		self._map.prepare()
		
		self.draw_board(w)
		
		#self.play_game()
		self.draw_status_area()
		self._roll_var = StringVar()
		
		self._roll_button = Button(
			frame1, 
			text="End turn", 
			command=self.roll,
			state=DISABLED # initially hide it
		)
		#self._roll_button.place(x=600, y=20)
		#self._roll_button.pack()
		
		self._canvas.create_window(60, self.height + 10, window=self._roll_button, anchor=S)
		
		l = Label(frame1, textvar=self._roll_var)
		l.place(x=600, y=120)
		l.pack()
		
		self.create_build_buttons()
		
		self._canvas.create_window(170, self.height + 10, window=self._build_buttons["settlement"], anchor=S)
		
		self.draw_robber()
		# bind events to the robber
		self._canvas.tag_bind("robber", "<ButtonPress-1>", self.grab_robber)
		self._canvas.tag_bind("robber", "<B1-Motion>", self.move_robber)
		self._canvas.tag_bind("robber", "<ButtonRelease-1>", self.release_robber)
		
		
		self.automate_setup()
	
	def play_dev_card(self):
		print "Playing dev card..."
		
	def buy_dev_card(self):
		'''User-triggered action to buy a development card.'''
		
		color = self.players[self._turn]
		card = self._map.get_development_card(color)
		
		if card is None:
			self.post_status_note("You cannot afford to buy a development card", True)
		else:
			self.post_status_note("Successfully bought development card")
			self.update_hand(color)
			print "{} bought development card '{}'".format(color, card)
			self.update_dev_cards(color)
		
	def create_build_buttons(self):
		'''Create buttons to build various structures.'''
		
		self._build_buttons = {}
		
		for type in ["road", "settlement", "city"]:
			self.create_build_button(type)
			
		self._build_buttons["dev_card"] = Button(
			self._frame, 
			text="Buy development card", 
			state=DISABLED, 
			command=self.buy_dev_card
		)
		
		self._build_buttons["dev_card"].pack()
			
		f = lambda: self.change_to_state("gameplay")
			
		self._cancel_building_button = Button(
			self._frame,
			text="Cancel building",
			state=DISABLED,
			command=f
		)
		self._cancel_building_button.pack()
		
	def enable_building(self, type):
		'''Enable building of buildings of certain type.
		NOTE: player has not been charged by this point.'''
		
		color = self.players[self._turn]
		p = self._map.get_player(color)
		if p.can_deduct_resources(CatanConstants.building_costs[type]):
			if type == "city":
				for item in self._canvas.find_withtag("settlement"):
					self._canvas.itemconfigure(item, state=NORMAL)
				for item in self._canvas.find_withtag("settlement_placeholder"):
					self._canvas.itemconfigure(item, state=DISABLED)
			else:
				for item in self._canvas.find_withtag(type + "_placeholder"):
					self._canvas.itemconfigure(item, state=NORMAL)
			
			self.change_to_state("place additional building")
		else:
			self.post_status_note("{} does not have enough resources to build a {}".format(color, type), True)
		
	def create_build_button(self, type):
		'''Create button to build given structure.'''
		
		f = lambda: self.enable_building(type)
		
		self._build_buttons[type] = Button(
			self._frame, 
			text="Build {}".format(type), 
			command=f,
			state=DISABLED
		)
		
		self._build_buttons[type].pack()
		
	def roll(self, change_turn=True):
		'''End the previous turn, then roll the dice.'''
		
		if change_turn:
			self.change_status_turn()
		
		# roll the dice
		r1 = random.randint(1, 6)
		r2 = random.randint(1, 6)
		n = r1 + r2
		
		# display dice roll
		self._roll_var.set("Last roll: {}".format(n))
		self.post_status_note("Rolled %d" % n)
		
		if n == 7:
			# TODO discard cards first
			
			# 
			self.change_to_state("move robber")
			# starting from this player, each player must discard half their hand
			#discard_map = self._map.robber_discard()
			
			#for i in range(4):
			#	c = self.players[self._turn]
			#	# this player must discard <x> cards
			#	if c in discard_map:
			#		self.post_status_note("{} must discard {} cards".format(c.title(), discard_map[c]), True)
		else:
			# compute produced resources
			d = self._map.get_resources_produced(n)
		
			# update player hands on screen
			for c in self.players:
				self.update_hand(c)
			
	def update_hand(self, color):
		'''Update the hand displayed for player of the given color.'''
		
		self._canvas.itemconfigure(
			self._hands[color], 
			text=self._map.get_player(color).get_printable_hand().replace(",", "\n")
		)
		
	def update_dev_cards(self, color):
		'''Update development card display in GUI for player of given color.'''
		
		item = self._canvas.find_withtag("dev_area_section_%s" % color)
		self._canvas.itemconfigure(
			item, 
			text=self._map.get_player(color).get_printable_dev_cards().replace(",", "\n")
		)
		
	def act_on_start_state(self):
		'''Set the state notification to the correct value.'''
		
		# set the note
		self.post_status_note()
		
		if self._state in ["first settlement placement", "second settlement placement"]:
			# disable roads
			for road in self._canvas.find_withtag("road"):
				self._canvas.itemconfigure(road, state=DISABLED)
			# disable city upgrades
			for s in self._canvas.find_withtag("settlement"):
				self._canvas.itemconfigure(s, state=DISABLED)
			for s in self._canvas.find_withtag("settlement_placeholder"):
				self._canvas.itemconfigure(s, state=NORMAL)
		elif self._state in ["first road placement", "second road placement"]:
			for s in self._canvas.find_withtag("settlement"):
				self._canvas.itemconfigure(s, state=DISABLED)
			for city in self._canvas.find_withtag("city"):
				self._canvas.itemconfigure(city, state=DISABLED)
		elif self._state == "gameplay":
			# disable all buildings
			for b in self._canvas.find_withtag("building"):
				self._canvas.itemconfigure(b, state=DISABLED)
			# re-enable the roll button
			self._roll_button.config(state=NORMAL)
			for button in self._build_buttons.itervalues():
				button.config(state=NORMAL)
		elif self._state == "choose building":
			# hide the hand while building
			for item in self._canvas.find_withtag("hand_area_section"):
				self._canvas.itemconfigure(item, state=HIDDEN)
				
			for item in self._canvas.find_withtag("building_selection"):
				self._canvas.itemconfigure(item, state=NORMAL)
			
		elif self._state == "place additional building":
			# disable the buttons
			self._roll_button.config(state=DISABLED)
			for b in self._build_buttons.itervalues():
				b.config(state=DISABLED)
			self._cancel_building_button.config(state=NORMAL)
			
		elif self._state == "move robber":
			self._roll_button.config(state=DISABLED)
			self._build_buttons["settlement"].config(state=DISABLED)
			for s in self._canvas.find_withtag("building"):
				self._canvas.itemconfigure(s, state=DISABLED)
			self._canvas.itemconfigure(self._robber_sprite, state=NORMAL)
			
			# TODO for now
			self.automate_robber()
			#self.change_to_state("gameplay")
			
		elif self._state == "choose player":
			# enable the hand rectangles
			#for rect in self._canvas.find_withtag("player_hand_rect"):
			#	self._canvas.itemconfigure(rect, state=NORMAL)
			self.enable_stealing()
				
			# disable basically everything else
			for s in self._canvas.find_withtag("building"):
				self._canvas.itemconfigure(s, state=DISABLED)
			self._roll_button.config(state=DISABLED)
			self._build_buttons["settlement"].config(state=DISABLED)
				
	def post_status_note(self, msg=None, error=False):
		c = "red" if error else "black" 
		
		if msg:
			t = msg
		else:
			if "settlement placement" in self._state:
				t = "Place a settlement"
			elif "road placement" in self._state:
				t = "Place a road"
			elif self._state == "gameplay":
			#	t = "Done with initial placements!"
				t = None
			elif self._state == "move robber":
				t = "Move the robber"
			elif self._state == "choose player":
				t = "Choose a player to steal from"	
			elif self._state == "choose building":
				t = "Choose a building to build"
			elif self._state == "place additional building":
				t = "Place the building you just bought"
			
		if t is not None:
			self._canvas.itemconfigure(self._note, text=t, fill=c)
				
	def act_on_end_state(self):
		'''Perform state exit actions.'''
		
		if self._state in ["first settlement placement", "second settlement placement"]:
			for road in self._canvas.find_withtag("road"):
				self._canvas.itemconfigure(road, state=NORMAL)
			for city in self._canvas.find_withtag("city"):
				self._canvas.itemconfigure(city, state=NORMAL)
				
			if "second" in self._state:
				# note the turn hasn't changed
				c = self.players[self._turn]
				second_settlement =  self._map.get_player(c).get_settlement(1)
				self._map.produce(second_settlement)
				self.update_hand(c)
		elif self._state in ["first road placement", "second road placement"]:
			for s in self._canvas.find_withtag("settlement"):
				self._canvas.itemconfigure(s, state=NORMAL)
			for city in self._canvas.find_withtag("city"):
				self._canvas.itemconfigure(city, state=NORMAL)
				
			if "second" in self._state and self._turn == 0:
				self.roll(False)
		elif self._state == "gameplay":
			# re-enable all buildings
			for b in self._canvas.find_withtag("building"):
				self._canvas.itemconfigure(b, state=NORMAL)
			self._roll_button.config(state=DISABLED)
		elif self._state == "move robber":
			self._roll_button.config(state=NORMAL)
			self._build_buttons["settlement"].config(state=NORMAL)
			for s in self._canvas.find_withtag("building"):
				self._canvas.itemconfigure(s, state=DISABLED)
			# this is the main part
			self._canvas.itemconfigure(self._robber_sprite, state=DISABLED)
		elif self._state == "choose player":
			for rect in self._canvas.find_withtag("player_hand_rect"):
				self._canvas.itemconfigure(rect, state=DISABLED)
			# the usual stuff will be enabled when gameplay is resumed
		elif self._state == "choose building":
			# hide the hand while building
			for item in self._canvas.find_withtag("hand_area_section"):
				self._canvas.itemconfigure(item, state=NORMAL)
				
			for item in self._canvas.find_withtag("building_selection"):
				self._canvas.itemconfigure(item, state=HIDDEN)
		elif self._state == "place additional building":
			# re-enable the buttons
			self._roll_button.config(state=NORMAL)
			for b in self._build_buttons.itervalues():
				b.config(state=NORMAL)
				
			# disable the buildings
			for s in self._canvas.find_withtag("building"):
				self._canvas.itemconfigure(s, state=DISABLED)
			self._cancel_building_button.config(state=DISABLED)
			
			# update hand
			self.update_hand(self.players[self._turn])
			
			# post that the building is good
			self.post_status_note("Successfully built the building!", False)
		
	def draw_build_menu(self):
		'''Draw the build menu.'''
		
		x = 550
		y_start = 100
		y_incr = 70
		build_list = ["settlement", "city", "road"]
		
		for i in range(len(build_list)):
			l = Label(self._canvas, text=build_list[i], anchor=W, font=("Helvetica", 16))
			
			w = self._canvas.create_window(
				x, 
				y_start + i * y_incr, 
				window=l, 
				state=HIDDEN, 
				tag="building_selection",
				anchor=W
			)
			
			# TODO - create sprite for it
			
			#b = self._canvas.bbox(w)
			#self._canvas.itemconfigure(b, outline="black")
		
	def draw_status_area(self):
		'''Draw whose turn it is in top-left corner.'''
		
		self.draw_build_menu()
		
		# this is where the turn is
		self._canvas.create_text(50, 30, text="Current turn:")
		self._status_rect = self._canvas.create_rectangle(
			50, 40, 80, 70, 
			fill=self.players[self._turn], 
			tag="status_rect"
		)
		
		# this is the notifications area
		self._note = self._canvas.create_text(580, 30, width=180)
		self._state = "first settlement placement"
		self.act_on_start_state()
		
		self.draw_hand_area()
		
	def update_vp(self, color):
		'''Update the victory points for a player of the given color.'''
		
		item = self._canvas.find_withtag("player_hand_rect_%s_text" % color)
		self._canvas.itemconfigure(item, text=str(self._map.get_player_vp(color)))
		
	def draw_hand_area_section(self, c, i):
		f = lambda event : self.steal_from_player(c)
		t = "player_hand_rect_%s" % c
		
		self._canvas.create_rectangle(
			550, 
			120 * (i + 1) - 20, 
			580, 
			120 * (i + 1) + 10, 
			fill=c,
			outline="black",
			tag=("player_hand_rect", t, "hand_area_section"),
			state=DISABLED,
			activeoutline="gold4",
		)
		
		self._canvas.create_text(
			565,
			120 * (i + 1) - 5,
			text="0",
			tag=t + "_text",
			font=("Helvetica", 14),
			fill="white"
		)
		
		self._hands[c] = self._canvas.create_text(
			670, 
			120 * (i + 1), 
			text="", 
			width=150, 
			justify=RIGHT,
			tag="hand_area_section"
		)
		self._canvas.tag_bind(t, "<Button>", f)
		
		self._canvas.create_text(
			770,
			120 * (i + 1),
			text="NO  DEV CARDS",
			width = 150,
			justify=RIGHT,
			tag="dev_area_section_%s" % c
		)
		
		# create a box around the whole thing
		self._canvas.create_rectangle(
			530,
			120 * (i + 1) - 50,
			850,
			120 * (i + 1) + 50,
			fill="",
			outline=c,
			tag="hand_area_section",
			width=1.5
		)
			
	def draw_hand_area(self):
		'''Draw the area where the hands for the players are displayed.'''
		
		self._hands = {}
		
		# this is where the cards are
		for i, c in enumerate(self.players):
			self.draw_hand_area_section(c, i)
			
		# and a box around everything
		self._canvas.create_rectangle(
			525,
			65,
			855,
			535,
			fill="",
			outline="black",
			width=1.5,
			dash=(2, 4)
		)
		
	def steal_from_player(self, from_player):
		'''Steal from player of given player color.
		Called by selecting player color in robber motion.'''
		
		to_player = self.players[self._turn]
		r = self._map.robber_steal(from_player, to_player)
		self.update_hand(from_player)
		self.update_hand(to_player)
		print "Stole {} from {} and gave to {}".format(r, from_player, to_player)
		self.post_status_note("Stole from {}".format(from_player))
		self.change_to_state("gameplay")
		
	def change_status_turn(self, decr=False):
		self._next_turn(decr)
		self._canvas.itemconfigure(self._status_rect, fill=self.players[self._turn])
		
	def draw_board(self, canvas):
		'''Now we draw the board...'''
		
		for row_i, row in enumerate(self._map.get_map()):
			for col, hex in enumerate(row):
				draw_hex(canvas, hex, row_i, col)
				
		self._settlements = {}
		self._roads = {}
		self._canvas = canvas
		for r in self._map.get_roads():
			self.draw_road_placeholder(canvas, r[0], r[1])
			
		for v in self._map.get_nodes():
			self.draw_settlement_node(canvas, v)
		
	def get_roads(self):
		return self._road_set
	
	def _get_road_slope(self, v1, v2):
		'''Return float.'''
		
		return (v2[1] - v1[1]) * 1.0 / (v2[0] - v2[1])
	
	def _get_y_offset(self, v1, v2, x_offset):
		return self._get_road_slope(v1, v2) * x_offset
			
	def draw_road_placeholder(self, canvas, v1, v2):
		''' Create an invisible, clickable road placeholder between v1 and v2.
		When clicked, it will act like a button and trigger an add_road event here.'''
		
		tag = "road_placeholder_{}_{}_{}_{}".format(v1[0], v1[1], v2[0], v2[1])
		
		# make sure v1 is the left-most one
		if v1[0] > v2[0]:
			v1, v2 = v2, v1
			
		spacing = 10
		slope = self._get_road_slope(v1, v2)
		x_offsets = [10, v2[0] - v1[0] - 10] # 10 from left and 10 from right
		y_offsets = [self._get_y_offset(v1, v2, dx) for dx in x_offsets]
		
		if slope == 0:
			# have a horizontal offset but no vertical offset
			v = (
				(v1[0] + 5, v1[1] + 5),
				(v2[0] - 5, v1[1] + 5),
				(v2[0] - 5, v2[1] - 5),
				(v1[0] + 5, v2[1] - 5),
				)
		elif slope > 0: # this corresponds to neg. slope because of how Tkinter coords work
			
			v = (
				(v1[0] - 5, v1[1]),
				(v1[0] + 5, v1[1]),
				(v2[0] + 5, v2[1]),
				(v2[0] - 5, v2[1]),
				)
		else: # neg. slope in Tkinter == pos. slope in life
			v = (
				(v1[0] - 5, v1[1]),
				(v1[0] + 5, v1[1]),
				(v2[0] + 5, v2[1]),
				(v2[0] - 5, v2[1]),
				)
		
		self._roads[(v1, v2)] = canvas.create_polygon(
			*CatanUtils.get_tkinter_coords(v),
			tags=("road", tag, "building", "road_placeholder"),
			fill="", # transparent fill
		   	outline="" # no outline
		)
		
		f = lambda e: self.add_road(e, v1, v2)
		canvas.tag_bind(tag, "<Button>", func=f)
		
	def add_road(self, event, v1, v2):
		'''Build a road between two vertices in response to user click.'''
		
		color = self.players[self._turn]
		
		# here we update the model
		if self._map.add_road(v1, v2, color):
			print "Building {} road between {} and {}".format(color, v1, v2)
			tag = "road_placeholder_{}_{}_{}_{}".format(v1[0], v1[1], v2[0], v2[1])
			
			self._canvas.itemconfigure(self._roads[(v1, v2)], fill=color, outline="black")
			self._canvas.dtag(self._roads[(v1, v2)], tag)
			self._canvas.dtag(self._roads[(v1, v2)], "road_placeholder")
			
			if self._state == "first road placement":
				if self._turn == 3:
					self.change_to_state("second settlement placement")
				else:
					self.change_status_turn()
					self.change_to_state("first settlement placement")
			elif self._state == "second road placement":
				if self._turn == 0:
					self.change_to_state("gameplay")
				else:
					self.change_status_turn(True)
					self.change_to_state("second settlement placement")
			else:
				self.change_to_state("gameplay")
		else:
			msg = "Road must be attached to a settlement."
			print "Failed to build {} road between {} and {}.".format(color, v1, v2) + " " + msg
			self.post_status_note(msg, error=True)
			
	def cull_adjacent_settlement_nodes(self, canvas, v):
		'''Remove all settlement nodes which are adjacent to vertex v.'''
		
		# first, get adjacent vertices
		adjacent_v_set = self._map.get_adjacent_vertices(v)
		for adjacent_v in adjacent_v_set:
			s = self._settlements[adjacent_v] # these are ovals
			canvas.delete(s) # delete the oval
	
	def draw_settlement_node(self, canvas, v):
		'''Draw an invisible settlement node at the given vertex.
		When clicked, it will act like a button and trigger a draw_settlement event here.'''
		
		padding = CatanApp.settlement_radius / 2
		tag = "settlement_oval_{}_{}".format(*v)
		self._settlements[v] = canvas.create_oval(
		   v[0] - padding,
		   v[1] - padding,
		   v[0] + padding,
		   v[1] + padding, 
		   tags=("settlement", tag, "settlement_placeholder", "building"),
		   fill="", # transparent fill
		   outline="" # no outline
		)
		
		# curry the function
		f = lambda e: self.add_settlement(e, v)
		canvas.tag_bind(tag, "<Button>", func=f)
	
	def add_settlement(self, event, v):
		'''Add a settlement at the given vertex in response to user click.'''
		
		color = self.players[self._get_turn()]
		
		# reflect changes in the model
		result = self._map.add_settlement(v, color)
		if result:
			print "Building {} settlement at {}".format(color, v)
			
			# TODO change the color to reflect the color of the player
			# TODO use sprites instead of ovals
			# draw the settlement oval
			self._canvas.itemconfigure(self._settlements[v], fill=color)
			self._canvas.itemconfigure(self._settlements[v], outline="black")
			self._canvas.dtag(self._settlements[v], "settlement_placeholder")
			
			# create new callback
			f = lambda e: self.add_city(e, v, color)
			tag = "settlement_oval_{}_{}".format(*v)
			self._canvas.tag_bind(tag, "<Button>", func=f)
			
			self.update_vp(color)
			
			if self._state == "first settlement placement":
				self.change_to_state("first road placement")
			elif self._state == "second settlement placement":
				self.change_to_state("second road placement")
			else:# self._state == "additional settlement placement":
				self.change_to_state("gameplay")
		else:
			print "Failed to build {} settlement at {}".format(color, v)
			#TODO possibly other error, like not being connected to a road
			#TODO this is a good place to use an exception
			self.post_status_note("That building spot is too close to an existing settlement", error=True)
			
	def change_to_state(self, new_state):
		'''Move from state in self._state to new_state.'''
		
		self.act_on_end_state()
		self._state = new_state
		self.act_on_start_state()
		
	def add_city(self, event, v, color):
		'''Add a city on top of existing settlement in response to user click.'''
		
		print "Building {} city at {}".format(color, v)
		
		# destroy the oval
		self._canvas.delete(self._settlements[v])
		
		padding = CatanApp.city_radius / 2
		tag = "city_square_{}_{}".format(*v)
		
		self._settlements[v] = self._canvas.create_rectangle(
		   v[0] - padding,
		   v[1] - padding,
		   v[0] + padding,
		   v[1] + padding, 
		   tags=("city", tag, "building"),
		   fill=color, # transparent fill
		   outline="black" # no outline
		)
		
		# reflect changes in the model
		self._map.add_city(v, color)
		self.update_vp(color)
		self.change_to_state("gameplay")
		
	@staticmethod
	def set_vertices(map):
		''' match the board with the generated map 
		'''
		
		# latice AKA board
		# TODO maybe calculate vertices rather than generating them all at once
		hex_coord_latice = get_hex_latice(
			CatanApp.render_start[0], 
			CatanApp.render_start[1], 
			CatanApp.minor_horiz_dist, 
			CatanApp.major_horiz_dist, 
			CatanApp.minor_vert_dist
		)
		
		for row_i, row in enumerate(map):
			for col_i, col in enumerate(row):
				col.set_vertices(hex_coord_latice[row_i][col_i])
		
	def key_pressed(self, event):
		print repr(event.char)

	def end(self, event):
		print "exit event captured"
		self._master.destroy()
		#sys_exit(0)
	
	def run(self):
		#TODO and the window is stuck at the front >_<  
		self._master.attributes("-topmost", 1) # this raises window to front
		
		#TODO focus is still not on top window
		
		self._master.mainloop()