Forum Archive

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

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!

JonB

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.

fergal

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.

JonB

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

JonB

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

fergal

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

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!

JonB

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.

JonB

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.

fergal

@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!

enceladus

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)