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
-
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/55695af0195c686f580f1d1faa04eaf6I 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.
-
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.
-
https://gist.github.com/a0fcfed33d13f3bc0de4051318042bb4
here is a slightly more animated roll, which includes some translation and rotational jitter. -
@JonB
https://gist.github.com/541ed070617b52aa3ee3271635036f10
and now with boggle letters... -
HOLY COW, ok, that's going to be a lot more to digest! Thanks again, can't wait to dive in.
-
@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/b96079da685fe3b5bca5407930155d7fMy 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!
-
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.
-
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.
-
@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!
-
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)