Beispiel #1
0
class App:
	'''The main card game application.  This GUI creates a Deck and Board
	model objects and uses them to keep track of legal moves, where the
	cards are on the board, etc., and then displays card images for the
	cards on a created BoardGui (the GUI view).
	'''

	def __init__(self, document, canv):
		'''Store the main window, create the Board and Deck models;
		create all the buttons and labels to allow the user to manipulate the game.
		'''

		self._doc = document
		self._canv = canv	# fabric Canvas object

		self._canv.on("mouse:up", self.onCardClick)

		self.loadCardMoveSound()
		self.loadCardInPlaceSound()
		self.loadEndOfRoundSound()
		self.loadFanfareSound()
		self._playSounds = True

		self._board = Board()
		self._deck = Deck()
		self._deck.addAllCards()
		self._copyOfDeck = self._deck.makeCopy()

		# We'll fill this in when we remove the aces from the board.
		self._removedAces = []

		# keeps track of cards that can be moved: each item is a tuple:
		# (card, row, col)
		self._moveableCards = []

		# A mapping from card object to CardImg object.  We do it this way
		# so that the card object (in the model) remains agnostic of the view
		# being used on it.
		self._card2ImgDict = {}

		self._score = 0
		# a list of templates of high scores we can update when high scores
		# change, so the screen changes immediately
		self._score_val = []
		self._storage = local_storage.storage
		self.loadSettingsFromStorage()

		# count of cards correctly placed, per round. 1st round is in
		# index 0, and initialized to 0 cards placed.
		self._cardsPlacedPerRound = [0]

		self._roundNum = 1

		# 2 rows of info about the game.
		self._game_info_elem = html.DIV(Class="game-info")
		self._doc <= self._game_info_elem
		self._game_info2_elem = html.DIV(Class="game-info")
		self._doc <= self._game_info2_elem

		self._next_round_btn = html.BUTTON(
			"Next Round", Class="button", disabled=True)
		self._next_round_btn.bind('click', self.nextRound)
		self._game_info_elem <= self._next_round_btn

		self._round_info_elem = \
			html.SPAN("Round: {roundNum}",
					  Class="info-text", id="round_num")
		self._game_info_elem <= self._round_info_elem
		self._round_num_val = template.Template(self._round_info_elem)
		self.updateRoundNum()

		self._cardsInPlace = self._board.countCardsInPlace()
		self._cards_in_place_elem = html.SPAN(
			"Cards in place: {cardsInPlace}", Class="info-text")
		self._game_info_elem <= self._cards_in_place_elem
		self._cardsInPlace_val = template.Template(self._cards_in_place_elem)
		self.updateCardsInPlace()

		self._score_info_elem = html.SPAN(
			"Score: {score}", Class="info-text")
		self._game_info_elem <= self._score_info_elem
		self._scoreInfo_val = template.Template(self._score_info_elem)

		self._pts_per_card_info_elem = html.SPAN(
			"Pts per card: {ptsPerCard}", Class="info-text")
		self._game_info_elem <= self._pts_per_card_info_elem
		self._ptsPerCardInfo_val = template.Template(
			self._pts_per_card_info_elem)
		self.updateScore()

		self._new_game_btn = html.BUTTON(
			"New Game", Class="button", disabled=True)
		self._new_game_btn.bind('click', self.newGameClickHandler)
		self._game_info_elem <= self._new_game_btn

		self._repeat_game_btn = html.BUTTON(
			"Repeat Game", Class="button")
		self._repeat_game_btn.bind('click', self.repeatGameClickHandler)
		self._game_info_elem <= self._repeat_game_btn

		self._messageDiv = self.createMessageDiv()

		switch_button = html.BUTTON ('Switch Deck', Class = 'button')
		switch_button.bind ('click', switch_card_sources)
		self._game_info_elem <= switch_button

		self._status_elem = html.SPAN("{status}")
		self._game_info2_elem <= self._status_elem
		self._status_val = template.Template(self._status_elem)
		self.setStatus("Cards placed this round: 0")

		self._playSoundsSpan = html.SPAN()
		self._game_info2_elem <= self._playSoundsSpan
		self._playSoundsLabel = html.LABEL("Play sounds: ")
		self._playSoundsSpan <= self._playSoundsLabel
		self._playSoundsCheckBox = html.INPUT(
			type="checkbox", checked=self._playSounds)
		self._playSoundsCheckBox.bind('click', self.togglePlaySounds)
		self._playSoundsSpan <= self._playSoundsCheckBox

		# A mapping from Card object to CardImg object.  This is needed so
		# that we map a card in the board layout to the CardImg, which should
		# then be placed at a certain location.  (We don't keep a reference to
		# the CardImg in Card because it just shouldn't know how it is displayed.)
		cards = self._deck.getCards()
		for card in cards:
			cardimg = CardImg(card, self._canv)
			self._card2ImgDict[id(card)] = cardimg

		self._boardGui = BoardGui(self._board, self._canv, self._card2ImgDict)

		self.loadHighScores()

		self.initNewGame()

	def initNewGame(self):

		self._board.layoutCards(self._deck)

		self._boardGui.displayLayout()
		self._cardsInPlace = self._board.countCardsInPlace()
		self._cardsPlacedPerRound = [self._cardsInPlace]

		self.updateCardsInPlace()
		self.updateScore()

		# Disable the "next round" button.
		self.disableNextRoundBtn()
		timer.set_timeout(self.removeAces, 1500)

	def removeAces(self):
		'''Go through the board and remove the aces from the board.
		Then hide the corresponding CardImgs in the BoardGui for the
		aces.
		'''
		self._removedAces = self._board.removeAces()
		for card in self._removedAces:
			self._card2ImgDict[id(card)].erase()

		self.enableNewGameButton()

		if self.isEndOfRoundOrGame():
			return

		self._moveableCards = self._board.findPlayableCards()
		self.highlightMovableCards()
		self.markGoodCards()

	def isEndOfRoundOrGame(self):
		'''Check if the game is over or the round is over.  Return True
		if it is.
		'''
		if self._board.gameCompletelyDone():
			score = self.currentScore()
			self.displayMessageOverCanvas(
				f"Congratulations!\n\nYou finished the game in {self._roundNum} rounds\n" +
				f"with a score of {score}.\n" +
				"Click 'New Game' to try again.", 5000)
			self.addScoreToHighScoresTable(score)
			self.setStatus("Cards placed this round: " +
						   str(self._cardsPlacedPerRound[self._roundNum - 1]))
			self.playFanfareSound()
			return True

		if not self._board.moreMoves():
			self.setStatus("No more moves")
			# enable the next round button by deleting the disabled attribute
			self.playEndOfRoundSound()
			self.enableNextRoundBtn()
		return False

	def onCardClick(self, event):
		'''Called back when a card is clicked to be moved into an open spot:
		Figure out when card was clicked using the _imgDict to map id to cardImg.
		Find the related card object in the board, and from that, the cards
		  current row/col.
		Check with the board object to see if the card can be moved and if so
		  what is the dest row/col.
		Move the card in the board and in the boardGui.
		Check if there are more moves, the game is done, etc.
		'''
		DEBUG and debug ('Got click!')

		# print("number of objects = ", len(self._canv.getObjects()))

		ptr = self._canv.getPointer(event)
		x = ptr.x
		y = ptr.y
		card = self.getClickedCard(x, y)
		if card is None:
			return

		DEBUG and debug ('got card')
		if self.cardIsMoveable(card):

			fromRow, fromCol = self._board.findCardLocation(card)
			DEBUG and debug ('got card location')

			res = self._board.getMoveableCardDest(card)
			if res is None:
				print("Cannot move that card.")
				return
			toRow, toCol = res   # split into the 2 parts.
			DEBUG and debug ('got card dest')

			self.eraseMovableCardHighlights()
			cardsInPlaceBeforeMove = self._cardsInPlace
			self._board.moveCard(card, fromRow, fromCol, toRow, toCol)
			self._boardGui.moveCard(card, toRow, toCol)
			self._cardsInPlace = self._board.countCardsInPlace()
			DEBUG and debug ('moved card')
			newCardsInPlace = self._cardsInPlace - cardsInPlaceBeforeMove
			if newCardsInPlace > 0:
				self.playCardInPlaceSound()
				DEBUG and debug ('played card in place sound')
				self._cardsPlacedPerRound[self._roundNum-1] += newCardsInPlace
				DEBUG and debug ('calced cardsPlacedPerRound')
				self.updateCardsInPlace()
				DEBUG and debug ('updates Cards in palce')
				self.updateScore()
				DEBUG and debug ('scores udpated')
				self.markGoodCards()
				DEBUG and debug ('marked good cards')
				self.setStatus("Cards placed this round: " +
							   str(self._cardsPlacedPerRound[self._roundNum - 1]))
			else:
				# just a normal move
				self.playCardMoveSound()

			DEBUG and debug ('checking if end of round or game')
			if self.isEndOfRoundOrGame():
				return

			DEBUG and debug ('updating movable cards')
			self._moveableCards = self._board.findPlayableCards()
			DEBUG and debug ('updated movable cards')
			self.highlightMovableCards()
			DEBUG and debug ('highlighted movable cards')

		else:
			# user clicked another card: so highlight the card it would
			# have to go to the right of.
			# E.g., you click 7D, highlight 6D
			(card, row, col) = self._board.findLowerCard(card)
			if card is not None:
				self.flashLowerCard(card, row, col)

	def toggle_card_type (self, ev):
		self.newGameClickHandler (ev, self._copyOfDeck)

	def repeatGameClickHandler(self, ev):
		self.newGameClickHandler(ev, self._copyOfDeck)

	def newGameClickHandler(self, ev, deck=None):
		'''Call back when New Game button is pressed.
		'''

		self.disableNewGameButton()
		self._boardGui.clear()

		self._roundNum = 1
		self.updateRoundNum()
		self._cardsPlacedPerRound = [0]

		# Add all the cards on the board to the deck.
		if deck:
			self._deck = deck
			self._copyOfDeck = self._deck.makeCopy()
		else:
			self._deck.addCards(self._board.getAllCards())
			if self._deck.numCards() == 48:
				# The board had aces removed, so add the aces back to the deck.
				for card in self._removedAces:
					self._deck.addCard(card)
			self._deck.shuffle()

		self._board.reinit()

		self.initNewGame()

	def nextRound(self, ev):
		'''Callback for when the user clicks the "Next Round" button.
		Increment the round number counter;
		Remove the cards from the board that are not in the correct place;
		Add those cards, and the aces, back to the deck; shuffle it;
		Update the display to show the good cards only, for 1 second;
		Register nextRoundContined() to be called.
		'''

		self.disableNewGameButton()

		# No cards placed yet in this round
		self._cardsPlacedPerRound.append(0)
		self._roundNum += 1
		self.updateRoundNum()
		discarded = self._board.removeIncorrectCards()
		self._deck.addCards(discarded)
		# Add the aces back to the deck.
		for card in self._removedAces:
			self._deck.addCard(card)
		self._deck.shuffle()

		# display the board with only "good cards" for 1 second.
		for card in discarded:
			cardimg = self._card2ImgDict[id(card)]
			cardimg.erase()
		self.updateScore()

		self.setStatus("Cards placed this round: " +
					   str(self._cardsPlacedPerRound[self._roundNum - 1]))

		timer.set_timeout(self.nextRoundContinued, 1000)

	def nextRoundContinued(self):
		'''Continuation of nextRound():
		Lay out all the cards from the deck on the board;
		Update the button states;
		Wait a bit, then call removeAces().
		'''

		# Deck is shuffled.  Now, add cards to the board.
		self._board.layoutCards(self._deck)
		self._boardGui.displayLayout()

		self.disableNextRoundBtn()
		# After 1.5 seconds of showing all cards, remove the aces.
		timer.set_timeout(self.removeAces, 1500)

	def getClickedCard(self, x, y):
		cards = self._board.getAllCards()
		for card in cards:
			cardImg = self._card2ImgDict[id(card)]
			if (x, y) in cardImg:
				return card
		return None

	def cardIsMoveable(self, card):
		for mcard, row, col in self._moveableCards:
			if mcard == card:
				return True
		return False

	def eraseMovableCardHighlights(self):
		for card, row, col in self._moveableCards:
			self.eraseOutline(card)

	def highlightMovableCards(self):
		'''Get the cards that have a space after them, find the cards
		that could go in those spaces, and draw a rectangle around those
		cards.
		'''
		for card, row, col in self._moveableCards:
			self.drawOutline(card, MOVABLE_CARD_COLOR)

	def markGoodCards(self):
		'''Redraw all the outlines around good cards.'''
		goodCards = self._board.getCardsInPlace()
		DEBUG and debug ('got cards in place')
		for card, r, c in goodCards:
			self.drawOutline(card, CARDS_IN_PLACE_COLOR)

	def flashLowerCard(self, card, row, col):
		cardimg = self._card2ImgDict[id(card)]
		cardimg.bounce()

	# def unflashLowerCard(self):
	#	 self.eraseOutline(self._flashingCard)
	#	 # the card that was flashed may have been one of these others
	#	 # so we have to redraw everything.
	#	 self.highlightMovableCards()
	#	 self.markGoodCards()

	def drawOutline(self, card, color):
		cardimg = self._card2ImgDict[id(card)]
		cardimg.displayOutline(color)

	def eraseOutline(self, card):
		cardimg = self._card2ImgDict[id(card)]
		cardimg.eraseOutline()

	def enableNextRoundBtn(self):
		del self._next_round_btn.attrs['disabled']

	def disableNextRoundBtn(self):
		self._next_round_btn.attrs['disabled'] = True

	def getPtsPerCard(self):
		'''10 pts for round 1, 9 for round 2, etc...'''
		return 11 - self._roundNum

	def currentScore(self):
		'''Compute the total score each time by summing the number of
		cards placed correctly in a round * the value of a card per round.'''
		res = 0
		for rnd in range(len(self._cardsPlacedPerRound)):
			res += (10 - rnd) * self._cardsPlacedPerRound[rnd]
		return res

	def updateScore(self):
		self._scoreInfo_val.render(score=self.currentScore())
		self._ptsPerCardInfo_val.render(ptsPerCard=self.getPtsPerCard())

	def updateCardsInPlace(self):
		self._cardsInPlace_val.render(cardsInPlace=self._cardsInPlace)

	def updateRoundNum(self):
		self._round_num_val.render(roundNum=self._roundNum)

	def setStatus(self, status):
		self._status_val.render(status=status)

	def enableNewGameButton(self):
		del self._new_game_btn.attrs['disabled']

	def disableNewGameButton(self):
		self._new_game_btn.attrs['disabled'] = True

	def createMessageDiv(self):
		elem = html.DIV(Class="message-box")
		elem.style.top = f"{CANVAS_HEIGHT / 3}px"
		elem.style.left = f"{CANVAS_WIDTH / 3}px"
		elem.style.width = f"{CANVAS_WIDTH / 3}px"
		# elem.style.height = f"{CANVAS_HEIGHT / 3}px"
		return elem

	def displayMessageOverCanvas(self, msg, ms):
		'''Split msg on newlines and put each result into its own div,
		which is then centered in the containing div, and displayed
		on over the canvas.  Undisplay it after *ms* milliseconds.
		'''
		self._messageDiv.style.display = "block"
		lines = msg.split('\n')
		htmls = [html.DIV(line, Class="center") for line in lines]
		for h in htmls:
			self._messageDiv <= h
		self._doc <= self._messageDiv
		timer.set_timeout(self.eraseMessageBox, ms)

	def eraseMessageBox(self):
		self._messageDiv.style.display = "none"
		self._messageDiv.clear()

	def loadCardMoveSound(self):
		self._moveCardSound = self.loadSound("snap3.wav")

	def loadCardInPlaceSound(self):
		self._cardInPlaceSound = self.loadSound("inplace.wav")

	def loadEndOfRoundSound(self):
		self._endOfRoundSound = self.loadSound("lose.wav")

	def loadFanfareSound(self):
		self._fanfareSound = self.loadSound("fanfare.wav")

	def loadSound(self, file):
		sound = document.createElement("audio")
		sound.src = file
		sound.setAttribute("preload", "auto")
		sound.setAttribute("controls", "none")
		sound.style.display = "none"
		self._doc <= sound
		return sound

	def playCardMoveSound(self):
		if self._playSounds:
			self._moveCardSound.play()

	def playCardInPlaceSound(self):
		if self._playSounds:
			self._cardInPlaceSound.play()

	def playEndOfRoundSound(self):
		if self._playSounds:
			self._endOfRoundSound.play()

	def playFanfareSound(self):
		if self._playSounds:
			self._fanfareSound.play()

	def togglePlaySounds(self, ev):
		self._playSounds = ev.target.checked
		self.storePlaySoundsSetting()

	def loadHighScores(self):
		# storing up to 5 high scores.
		self._score = [0] * 5
		try:   # there may be less than 5 values stored so far.
			for i in range(5):
				self._score[i] = int(self._storage[f'highScore{i}'])
		except:
			pass
		self._scores_span = html.SPAN()
		self._game_info2_elem <= self._scores_span

		header = html.SPAN('High Scores: ')
		self._scores_span <= header

		for i in range(5):
			span = html.SPAN("{score}")
			self._score_val.append(template.Template(span))
			self._scores_span <= span
			self._score_val[i].render(score=self._score[i])

	def addScoreToHighScoresTable(self, score):
		changed = False
		for i in range(5):
			if score >= self._score[i]:
				self._score.insert(i, score)
				changed = True
				break
		if changed:
			for i in range(5):
				self._storage[f'highScore{i}'] = str(self._score[i])
				# update the screen
				self._score_val[i].render(score=self._score[i])

	def loadSettingsFromStorage(self):
		try:
			self._playSounds = self._storage['playSounds'] == "True"
		except:
			print("no playsounds in storage")
			# If we haven't stored a choice, the default is True
			self._playSounds = True

	def storePlaySoundsSetting(self):
		self._storage['playSounds'] = str(self._playSounds)