omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular

    Welcome!

    This is the community forum for my apps Pythonista and Editorial.

    For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.


    Boggle App, need help changing labels 3x in a function

    Pythonista
    3
    11
    6712
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • fergal
      fergal last edited by fergal

      I've got a working boggle app as my first pythonista project, I like it and it's functional, but I want to add some pizzaz to it and that's where I'm having trouble.

      You can see the whole (very simple) project at https://github.com/rgregory1/boggle_pythonista

      I have a function called button_press that rolls the dice and adds text to the 'dice' labels. I want to change the function to show a random assortment of dice once a second for 3 times, then leave the last one there for the game. I can make it happen, but it won't show the dice until the function finishes, so in effect is that it blanks for 3 seconds then continues.

      I managed to run a timer at the same time as my other functions with the @ui.in_background decorator, but that won't work for this one for some reason.

      Here is my code (shortened for 4 dice rather than 16 for brevity)

      def button_press(self):
      	print('button was pressed')
      	self.title = "Play Game"
      	dice_rolls = roll_all_dice(dice)
      	label1.text = str(dice_rolls[0])
      	label2.text = str(dice_rolls[1])
      	label3.text = str(dice_rolls[2])
      	label4.text = str(dice_rolls[3])
      	with open('dice_rolls.json', 'w') as f:
      		json.dump(dice_rolls, f)
      	countdown()
      	main_button.action = show_letters
      

      The effect I want would be this:

      def button_press(self):
      	print('button was pressed')
      	self.title = "Play Game"
      	dice_rolls = roll_all_dice(dice)
      	label1.text = str(dice_rolls[0])
      	label2.text = str(dice_rolls[1])
      	label3.text = str(dice_rolls[2])
      	label4.text = str(dice_rolls[3])
          time.sleep(1)
          dice_rolls = roll_all_dice(dice)
      	label1.text = str(dice_rolls[0])
      	label2.text = str(dice_rolls[1])
      	label3.text = str(dice_rolls[2])
      	label4.text = str(dice_rolls[3])
          time.sleep(1)
          dice_rolls = roll_all_dice(dice)
      	label1.text = str(dice_rolls[0])
      	label2.text = str(dice_rolls[1])
      	label3.text = str(dice_rolls[2])
      	label4.text = str(dice_rolls[3])
      	with open('dice_rolls.json', 'w') as f:
      		json.dump(dice_rolls, f)
      	countdown()
      	main_button.action = show_letters
      

      I tried moving this chunk to a function to make it look better,

      dice_rolls = roll_all_dice(dice)
      	label1.text = str(dice_rolls[0])
      	label2.text = str(dice_rolls[1])
      	label3.text = str(dice_rolls[2])
      	label4.text = str(dice_rolls[3])
      

      but I thought I'd leave it in a block to try and problem solve it?

      Thanks for any help!

      1 Reply Last reply Reply Quote 0
      • JonB
        JonB last edited by

        With ui, nothing is shown in your ui until your function ends and returns control to the ui thread.

        The ui.in_background is a little confusing because it can only run one thing in the background at a time, including whatever code you have in your main script.

        The trick is to use either ui.delay, or ui.animate, which run on the main(ui) thread. In this case, animate is probably what you want, though it is a little trickier to string together animations, since you have to set the completion animation in the arguments, but a little redursive helper makes it easy to call.

        Here is a simple proof of concept:
        https://gist.github.com/55695af0195c686f580f1d1faa04eaf6

        I was lazy, and made pressing an individual dice roll them all, but you pribably want a separate button for that, and a dufferent action when pressing the dice, but this shows the basic idea.

        For what its worth, this also shows how to avoid having to individually name every label -- the same code works for a 3x3 grid or a 10x10 grid.

        1 Reply Last reply Reply Quote 0
        • fergal
          fergal last edited by

          Wow JonB! I'm blown away by the detailed and awesome response!

          I'm at work now but I will try to dissect and understand this the best I can tonight when I get home! As you can tell by my code, I'm just beginning my python journey and my Pythonista journey as well. This should help a great deal.

          1 Reply Last reply Reply Quote 0
          • JonB
            JonB last edited by

            https://gist.github.com/a0fcfed33d13f3bc0de4051318042bb4
            here is a slightly more animated roll, which includes some translation and rotational jitter.

            JonB 1 Reply Last reply Reply Quote 0
            • JonB
              JonB @JonB last edited by

              @JonB
              https://gist.github.com/541ed070617b52aa3ee3271635036f10
              and now with boggle letters...

              1 Reply Last reply Reply Quote 0
              • fergal
                fergal last edited by

                HOLY COW, ok, that's going to be a lot more to digest! Thanks again, can't wait to dive in.

                1 Reply Last reply Reply Quote 0
                • fergal
                  fergal last edited by fergal

                  @JonB
                  Ok JonB, I've been working on understanding what is going on and how I can implement this awesome work you've showed me.

                  For me, the important part is that I understand each piece so I know how it works, so I do that by building the script up slowly and checking that I understand each piece as I do it.

                  I started with creating the 'dice' here. You used buttons and were designing for an iphone screen size. I'm starting with an iPad size and moving to iPhone afterwards. So my first question is, can I work on the animations if the 'dice' are labels rather than buttons?

                  The only reason that I am looking for labels over animation is that I want to be able to change the text size and weight and I'm not sure how to do that with buttons. BUT I can't change the corner radius with labels, so I'm kinda stuck.
                  Here is what I have so far,
                  https://gist.github.com/rgregory1/b96079da685fe3b5bca5407930155d7f

                  My game plan is:

                  • achieve the look I'm after
                  • make them roll correctly
                  • begin to understand the animation

                  So knowing that, do I need to make the 'dice' buttons rather than labels?

                  Thanks for any help!

                  1 Reply Last reply Reply Quote 0
                  • JonB
                    JonB last edited by

                    Buttons do have a font attribute, that works just like label. You use the tint attribute to change text color. labels have the corner radius "bug", but you could also have a View with a Label subview -- which is basically what a button does internally. Either approach can be animated the same way.

                    What might make a lot of sense is to define a custom subclass of View with a custom draw method, which would allow creating a"shaded" die. I'll post an experiment when I get home where the die consists of two half-height rounded rectangles of different shade, then a lighter circle on top for the face. Looks pleasing, and can still be animated.

                    Is your goal to create just the game board, and individual players use paper? Or will this be a solo game, where you want to be able to tap letter sequences to make words? If the latter, then button or custom view will be the way you want to go.

                    Final thought... The roll animation looks very 2d... If you go to some sort of drawn die or image, one could start to imagine a few "tween" images of a cube mid-roll. More complex, but depend on how much sugar you want.

                    1 Reply Last reply Reply Quote 0
                    • JonB
                      JonB last edited by

                      https://gist.github.com/3dbd60b05e13fa32a497122ff9754d7b
                      Here is a completely different take, a little more organized into classes. I tried to draw a more realistic dice image, and added scale (size) into the mix of things getting animated, so it feels more like the dice are moving in and out of the page... sort of. i tried to comment what i was doing.

                      sorry for hijacking your project... it was fun to experiment with.

                      1 Reply Last reply Reply Quote 0
                      • fergal
                        fergal last edited by

                        @JonB
                        No problem on the hijacking! I think it's great that you ran with it, I picked it because it is an achievable project for me, but also because it seems like a fun programming challenge. It was my first CLI challenge I set myself last year!

                        There are a million one player versions out there, my family really enjoys board games, so I want to be able to pull out my phone or ipad at the restaurant while waiting for food and play boggle with my daughters, we'll use pen and napkins for words. Keeping a score tally might be a fun activity to add in afterwards.

                        Your idea for the dice is great, I was going to try rounded corners with a circle in the middle as my final goal, so this is great.

                        Your code looks BRILLIANT, I can't wait to dig into it. Thank you for breaking it up for me to digest. I usually feel that one good example that I can understand will feed me for weeks!

                        Again, I appreciate it!

                        1 Reply Last reply Reply Quote 0
                        • enceladus
                          enceladus last edited by

                          The following word game by Omz was in the examples directory of old version of Pythonista. I have modified it slightly to run on new version (python 3).

                          # coding: utf-8
                          from scene import *
                          import ui
                          import sound
                          import marshal
                          import string
                          import time
                          from itertools import product, chain
                          from random import choice, random
                          from copy import copy
                          import os
                          from math import sqrt
                          import json
                          A = Action
                          
                          game_duration = 90
                          screen_w, screen_h = get_screen_size()
                          min_screen = min(screen_w, screen_h)
                          if max(get_screen_size()) >= 760:
                          	cols, rows = 10, 11
                          else:
                          	cols, rows = 7, 7
                          tile_size = min_screen / (max(cols, rows) + 1)
                          font_size = int(tile_size * 0.6)
                          tile_font = ('AvenirNext-Regular', font_size)
                          score_font = ('AvenirNext-Regular', 50)
                          time_font = ('AvenirNext-Regular', 32)
                          preview_font = ('AvenirNext-Regular', 24)
                          game_over_font = ('AvenirNext-Regular', 72)
                          points_font = ('AvenirNextCondensed-Regular', 32)
                          
                          # Derived from http://en.m.wikipedia.org/wiki/Letter_frequency
                          letter_freq = {'a': 8.2, 'b': 1.5, 'c': 2.8, 'd': 4.3, 'e': 12.7, 'f': 2.3, 'g': 2.0, 'h': 6.1, 'i': 7.0, 'j': 0.2, 'k': 7.7, 'l': 4.0, 'm': 2.4, 'n': 6.7, 'o': 7.5, 'p': 1.9, 'q': 0.1, 'r': 6.0, 's': 6.3, 't': 9.0, 'u': 2.8, 'v': 1.0, 'w': 2.4, 'x': 0.2, 'y': 2.0, 'z': 0.1}
                          letter_bag = list(chain(*[[letter] * int(letter_freq[letter]*10) for letter in letter_freq]))
                          
                          def build_dictionary():
                          	# Generate the word list if it doesn't exist yet.
                          	# It's represented as a set for fast lookup, and saved to disk using the `marshal` module.
                          	if os.path.exists('words.data'):
                          		return
                          	#import urllib
                          	import requests
                          	words = []
                          	#f = urllib.urlopen('https://github.com/atebits/Words/blob/master/Words/en.txt?raw=true')
                          	f = str(requests.get('https://github.com/atebits/Words/blob/master/Words/en.txt?raw=true').text)
                          	for line in f.split():
                          		words.append(line.strip())
                          	with open('words.data', 'w') as out:
                          		#marshal.dump(words, out)
                          		json.dump(words, out)
                          
                          with ui.ImageContext(tile_size, tile_size) as ctx:
                          	ui.set_color('silver')
                          	ui.Path.rounded_rect(2, 2, tile_size-4, tile_size-4, 4).fill()
                          	ui.set_color('white')
                          	ui.Path.rounded_rect(2, 2, tile_size-4, tile_size-6, 4).fill()
                          	tile_texture = Texture(ctx.get_image())
                          
                          class Tile (SpriteNode):
                          	def __init__(self, x, y, letter, color='white', multiplier=1):
                          		SpriteNode.__init__(self, tile_texture)
                          		self.x = x
                          		self.y = y
                          		self.letter = letter
                          		self._selected = False
                          		pos_y = y * tile_size + (tile_size/2 if x % 2 == 0 else 0)
                          		self.position = x * tile_size, pos_y
                          		self.tile_color = color
                          		self.color = color
                          		self.label = LabelNode(letter.upper(), font=tile_font)
                          		self.label.color = 'black'
                          		self.multiplier = multiplier
                          		self.add_child(self.label)
                          	
                          	@property
                          	def selected(self):
                          		return self._selected
                          	
                          	@selected.setter
                          	def selected(self, value):
                          		self._selected = value
                          		self.color = '#fdffce' if value else self.tile_color
                          
                          class Game (Scene):
                          	def setup(self):
                          		self.current_size = None
                          		self.current_word = None
                          		build_dictionary()
                          		with open('words.data') as f:
                          			#self.words = marshal.load(f)
                          			self.words = set(json.load(f))
                          		self.root = Node(parent=self)
                          		self.background_color = '#0f2634'
                          		self.tiles = []
                          		self.selected = []
                          		self.touched_tile = None
                          		self.score_label = LabelNode('0', font=score_font, parent=self)
                          		self.score = 0
                          		self.game_over = False
                          		self.game_over_time = 0
                          		self.word_label = LabelNode(font=preview_font, parent=self)
                          		self.time_label = LabelNode('00:00', font=time_font, parent=self)
                          		self.time_label.anchor_point = (0, 0.5)
                          		self.start_time = time.time()
                          		self.overlay = SpriteNode(color='black', alpha=0, z_position=3, parent=self)
                          		time_up_label = LabelNode('Time Up!', font=game_over_font, parent=self.overlay)
                          		self.did_change_size()
                          		self.new_game()
                          	
                          	def create_tile(self, x, y):
                          		letter = choice(letter_bag)
                          		bonus = random() < 0.07
                          		t = Tile(x, y, letter, '#cef9ff' if bonus else 'white', 2 if bonus else 1)
                          		return t
                          		
                          	def did_change_size(self):
                          		x_margin = (self.size.w - cols * tile_size)/2
                          		y_margin = (self.size.h - rows * tile_size)/2 - tile_size/2
                          		self.root.position = x_margin + tile_size/2, y_margin + tile_size/2
                          		self.overlay.position = self.size/2
                          		self.overlay.size = self.size
                          		if self.size.w < self.size.h:
                          			self.score_label.position = self.size.w/2, self.size.h - 100
                          			self.word_label.position = self.size.w/2, self.size.h - 140
                          			self.time_label.position = 20, self.size.h - 100
                          			self.time_label.anchor_point = (0, 0.5)
                          		else:
                          			self.score_label.position = x_margin/2, self.size.h - 100
                          			self.word_label.position = x_margin/2, self.size.h - 140
                          			self.time_label.position = self.size - (20, 100)
                          			self.time_label.anchor_point = (1, 0.5)
                          		
                          	def update(self):
                          		time_passed = time.time() - self.start_time
                          		t = max(0, int(game_duration - time_passed))
                          		self.time_label.text = '{0}:{1:0>2}'.format(t//60, t%60)
                          		if t == 0 and not self.game_over:
                          			self.end_game()
                          	
                          	def new_game(self, animated=False):
                          		if self.game_over:
                          			self.play_sound('digital:ZapThreeToneUp')
                          		for tile in self.tiles:
                          			tile.remove_from_parent()
                          		self.tiles = []
                          		for x, y in product(range(cols), range(rows)):
                          			s = self.create_tile(x, y)
                          			if animated:
                          				s.scale = 0
                          				s.run_action(Action.scale_to(1, 0.5, TIMING_EASE_OUT_2))
                          			self.tiles.append(s)
                          			self.root.add_child(s)
                          		self.game_over = False
                          		self.start_time = time.time()
                          		self.score = 0
                          		self.score_label.text = '0'
                          		self.overlay.run_action(Action.fade_to(0))
                          		self.word_label.text = ''
                          		self.selected = []
                          		
                          	def end_game(self):
                          		self.play_sound('digital:ZapThreeToneDown')
                          		self.game_over = True
                          		self.game_over_time = time.time()
                          		self.overlay.run_action(Action.fade_to(0.7))
                          	
                          	def get_selected_word(self):
                          		return ''.join([t.letter for t in self.selected]).lower()
                          	
                          	def touch_to_tile(self, location):
                          		touch_x = location[0] - self.root.position[0]
                          		touch_y = location[1] - self.root.position[1]
                          		x = int(touch_x / tile_size)
                          		if x % 2 != 0:
                          			y = int((touch_y - tile_size/2) / tile_size)
                          		else:
                          			y = int(touch_y / tile_size)
                          		return x, y
                          	
                          	def tile_at(self, location):
                          		x = location[0] - self.root.position[0]
                          		y = location[1] - self.root.position[1]
                          		for tile in self.tiles:
                          			if abs(tile.position - (x, y)) < tile_size/2:
                          				if tile.alpha < 1:
                          					continue
                          				return tile
                          		return None
                          	
                          	def is_neighbor(self, tile1, tile2):
                          		if (not tile1 or not tile2 or tile1 == tile2):
                          			return True
                          		x1, y1 = tile1.x, tile1.y
                          		x2, y2 = tile2.x, tile2.y
                          		if x1 == x2:
                          			return abs(y2-y1) <= 1
                          		elif x1 % 2 == 0:
                          			return abs(x2-x1) <= 1 and 0 <= (y2-y1) <= 1
                          		else:
                          			return abs(x2-x1) <= 1 and -1 <= (y2-y1) <= 0
                          	
                          	def select_tile(self, tile):
                          		if not tile or (self.selected and self.selected[-1] == tile):
                          			return
                          		if tile in self.selected:
                          			self.selected = self.selected[:self.selected.index(tile)+1]
                          		else:
                          			if self.selected:
                          				last_selected = self.selected[-1]
                          				if not self.is_neighbor(tile, last_selected):
                          					self.selected = []
                          				self.selected.append(tile)
                          			else:
                          				self.selected = [tile]
                          		for tile in self.tiles:
                          			tile.selected =  (tile in self.selected)
                          		self.play_sound('8ve:8ve-tap-resonant')
                          		self.update_word_label()
                          	
                          	def update_word_label(self):
                          		word = self.get_selected_word()
                          		if word != self.current_word:
                          			self.word_label.color = '#ddffc2' if word in self.words else '#ffcece'
                          			self.word_label.text = word.upper()
                          			self.current_word = word
                          	
                          	def calc_score(self, word_tiles):
                          		n = len(word_tiles)
                          		multiplier = 1
                          		for tile in word_tiles:
                          			multiplier *= tile.multiplier
                          		return int((2 ** (n-2)) * 50 * multiplier)
                          	
                          	def submit_word(self):
                          		word = self.get_selected_word()
                          		if not word:
                          			return
                          		if word in self.words:
                          			added_score = self.calc_score(self.selected)
                          			self.score += added_score
                          			self.score_label.text = str(self.score)
                          			self.play_sound('digital:PowerUp7')
                          		else:
                          			added_score = 0
                          			self.play_sound('digital:PepSound4')
                          			for tile in self.selected:
                          				tile.selected = False
                          			self.selected = []
                          			self.touched_tile = None
                          		for tile in self.selected:
                          			tile.run_action(A.group(A.fade_to(0, 0.25), A.scale_to(0.5, 0.25)))
                          		self.tiles[:] = [t for t in self.tiles if t not in self.selected]
                          		sorted_selection = sorted(self.selected, key=lambda t: t.y, reverse=True)
                          		new_tiles_by_col = [0] * cols
                          		offsets = [0] * len(self.tiles)
                          		for t in sorted_selection:
                          			x, y = t.x, t.y
                          			new_tiles_by_col[x] += 1
                          			for i, tile in enumerate(self.tiles):
                          				if tile.x == x and tile.y > y:
                          					tile.y -= 1
                          					offsets[i] += 1
                          		for i, offset in enumerate(offsets):
                          			if offset > 0:
                          				tile = self.tiles[i]
                          				d = sqrt(offset * tile_size/750.0)
                          				tile.run_action(A.move_by(0, -offset*tile_size, d, TIMING_EASE_IN_2))
                          		for i, n in enumerate(new_tiles_by_col):
                          			for j in range(n):
                          				s = self.create_tile(i, rows-j-1)
                          				to_pos = s.position
                          				from_pos = to_pos[0], (rows + n-j) * tile_size
                          				s.position = from_pos
                          				self.tiles.append(s)
                          				self.root.add_child(s)
                          				s.alpha = 0
                          				s.run_action(Action.fade_to(1, 0.25))
                          				s.run_action(Action.move_to(to_pos[0], to_pos[1], sqrt((from_pos[1] - to_pos[1])/750.0), TIMING_EASE_IN_2))
                          		if added_score > 0:
                          			self.show_points(self.selected[-1].position, added_score)
                          		self.selected = []
                          		self.touched_tile = None
                          		self.update_word_label()
                          		
                          	def show_points(self, position, added_score):
                          		points_bg = ShapeNode(ui.Path.oval(0, 0, 100, 100), '#49b8ff', alpha=0)
                          		points_bg.position = position
                          		points_label = LabelNode('+%i' % (added_score,), font=points_font, parent=points_bg)
                          		points_bg.run_action(A.sequence(
                          			A.fade_to(1, 0.25),
                          			A.wait(0.5),
                          			A.fade_to(0, 0.5, TIMING_EASE_IN),
                          			A.remove()
                          		))
                          		points_bg.run_action(Action.move_by(0, 100, 1.5))
                          		self.root.add_child(points_bg)
                          	
                          	def touch_began(self, touch):
                          		if self.game_over:
                          			return
                          		prev_touched_tile = self.touched_tile
                          		self.touched_tile = self.tile_at(touch.location)
                          		if prev_touched_tile == self.touched_tile:
                          			self.submit_word()
                          		elif self.touched_tile:
                          			self.select_tile(self.touched_tile)
                          	
                          	def touch_moved(self, touch):
                          		if not self.game_over:
                          			self.select_tile(self.tile_at(touch.location))
                          	
                          	def touch_ended(self, touch):
                          		if self.game_over:
                          			if time.time() - self.game_over_time > 2.0:
                          				self.new_game(animated=True)
                          			return
                          		tile = self.tile_at(touch.location)
                          		if tile == self.touched_tile:
                          			self.select_tile(tile)
                          		else:
                          			self.submit_word()
                          	
                          	def play_sound(self, name):
                          		sound.play_effect(name)
                          
                          if __name__ == '__main__':
                          	run(Game(), multi_touch=False)
                          	
                          
                          
                          1 Reply Last reply Reply Quote 0
                          • First post
                            Last post
                          Powered by NodeBB Forums | Contributors