Forum Archive

Cloud Jump Game

bashedcrab

https://gist.github.com/9098744

I've been wanting to do something with the accelerometer for a while, and then I saw this game on the Codea demo movie.

I've always though doodle jump was a cool game, so here it is on Pythonista.

It would be cool if someone made the clouds look better, though.

brumm

E.g. you could resize the built-in 'Cloud'!?

Sebastian

I tried to make the clouds as it was made on the Codea game:


from scene import *
import random

IMAGE_WIDTH = 101
IMAGE_HEIGHT = 171
IMAGE_Y_OFFSET = -30
BLOCK_HEIGHT = 40
BLOCK_DEPTH = 80
DEAD_ZONE_MIN = -0.02
DEAD_ZONE_MAX =  0.02
PLAYER_CONTROL_SPEED = 2000
PLAYER_BOUNCE_VELOCITY = 1700
PLAYER_INITIAL_BOUNCE = 1700
MAX_CLOUD_DIST = 500
DIFFICULTY_Q = 100000.0
GAME_GRAVITY = 2000
GAME_WAITING = 0
GAME_PLAYING = 1
GAME_DEAD = 2

class Player(object):
    def __init__(self):
        self.bounds = Rect()
        self.velocity = 0

    def draw(self):
        tint(1,1,1)
        image('PC_Character_Pink_Girl', self.bounds.x, self.bounds.y + IMAGE_Y_OFFSET)

class GroundBlock(object):
    def __init__(self):
        self.bounds = Rect()

    def draw(self):
        tint(1,1,1)
        image('PC_Grass_Block', self.bounds.x, self.bounds.y)

class Cloud (object):
    def __init__(self):
        self.shapes = []

        num_circles = random.randint(4, 5)
        for i in xrange(num_circles):
            x = i * 20 - ((num_circles/2)*30)
            y = (random.random()-0.5) * 30
            rad = random.randint(50, 100)
            self.shapes.append([x, y, rad])

        self.width = num_circles * 30 + 30
        self.bounds = Rect(0, 0, self.width, 60)

    def is_colliding(self, pos):
        startp = self.bounds.x - self.width/2
        endp = self.bounds.x + self.width/2
        if ((pos.x < endp) and (pos.x > startp) and
            (pos.y < (self.bounds.y + 30)) and
            (pos.y > (self.bounds.y + 10))):
                return True 
        return False

    def draw(self):
        push_matrix()
        translate(self.bounds.x, self.bounds.y)
        no_stroke()
        fill(0.90, 0.90, 0.90)
        for i in self.shapes:
            ellipse(i[0], i[1] - 5, i[2], i[2])

        fill(1.00, 1.00, 1.00)
        for i in self.shapes:
            ellipse(i[0], i[1] + 5, i[2], i[2])

        pop_matrix()

class MyScene (Scene):

    def create_ground(self):
        for x in range((int(self.bounds.w) / IMAGE_WIDTH) + 1):
            block = GroundBlock()
            block.bounds = Rect(x * IMAGE_WIDTH, 0, IMAGE_WIDTH, IMAGE_HEIGHT)
            self.scenery.append(block)

    def generate_clouds(self):
        y = self.cloud_height
        while self.cloud_height < self.bounds.h * 2:
            q = min(self.climb, DIFFICULTY_Q)
            min_dist = int(MAX_CLOUD_DIST * q / DIFFICULTY_Q)
            max_dist = int(MAX_CLOUD_DIST / 2 + min_dist / 2)
            self.cloud_height += random.randint(min_dist, max_dist)
            cloud = Cloud()
            cloud.bounds.x = random.random() * (self.bounds.w - 150)
            cloud.bounds.y = self.cloud_height
            self.scenery.append(cloud)

    def cull_scenery(self):
        i = len(self.scenery)
        for sprite in self.scenery[:]:
            if sprite.bounds.top() < 0:
                self.scenery.remove(sprite)         

    def control_player(self):
        tilt = gravity().x
        if(tilt < DEAD_ZONE_MIN) or (tilt > DEAD_ZONE_MAX):
            move = self.dt * tilt * PLAYER_CONTROL_SPEED
            self.player.bounds.x += move
            if(self.player.bounds.x < 0):
                self.player.bounds.x = 0
            elif(self.player.bounds.x > self.bounds.w - self.player.bounds.w):
                self.player.bounds.x = self.bounds.w - self.player.bounds.w

    def lower_scenery(self, y):
        self.climb += y
        self.cloud_height -= y
        for sprite in self.scenery:
            sprite.bounds.y -= y

    def run_gravity(self):
        player_y_move = self.dt * self.player.velocity
        scenery_y_move = 0
        old_velocity = self.player.velocity
        self.player.velocity -= self.dt * GAME_GRAVITY
        if(old_velocity > 0) and (self.player.velocity <= 0):
            self.player_apex_frame = True
        self.player.bounds.y += player_y_move
        if(self.player.bounds.y >= self.player_max_y):
            scenery_y_move = self.player.bounds.y - self.player_max_y
            self.player.bounds.y = self.player_max_y
            self.lower_scenery(scenery_y_move)
        elif(self.player.bounds.top() < 0):
            self.game_state = GAME_DEAD

    def collision_detect(self):
        bounce = False
        if(self.player.velocity < 0):
            p = Point(self.player.bounds.x + self.player.bounds.w/2, self.player.bounds.y)
            for sprite in self.scenery:
                if hasattr(sprite, 'is_colliding'):
                    collision = sprite.is_colliding(p)
                else:
                    collision = p in sprite.bounds
                if collision:
                    self.player.velocity = PLAYER_BOUNCE_VELOCITY
                    break 

    def game_loop(self):
        if self.game_state == GAME_PLAYING:
            self.run_gravity()
            self.collision_detect()
            self.control_player()
            if self.player_apex_frame:
                self.cull_scenery()
                self.generate_clouds()
                self.player_apex_frame = False

    def shadow_text(self, s, x, y):
        tint(0,0,0)
        text(s, 'AvenirNext-Heavy', 48, x + 2, y - 2)
        tint(0.00, 0.50, 1.00)
        text(s, 'AvenirNext-Heavy', 48, x, y)

    def draw_text(self):
        if(self.game_state == GAME_WAITING):
            if(int(self.t) % 2):
                self.shadow_text('Tap Screen to Start', self.bounds.w / 2, self.bounds.h * 0.6)
                self.shadow_text('Tilt Screen to Steer', self.bounds.w / 2, self.bounds.h * 0.4)
        elif(self.game_state == GAME_PLAYING):
            self.shadow_text('Score : ' + str(int(self.climb / 10)), self.bounds.w / 2, self.bounds.h * 0.95)
        if(self.game_state == GAME_DEAD):
            self.shadow_text('Score : ' + str(int(self.climb / 10)), self.bounds.w / 2, self.bounds.h * 0.95)
            self.shadow_text('Game Over', self.bounds.w / 2, self.bounds.h / 2)


    def setup(self):
        self.game_state = GAME_WAITING
        self.scenery = []
        self.climb = 0
        self.create_ground()
        self.cloud_height = 200
        self.generate_clouds()
        self.player = Player()
        self.player_apex_frame = False
        self.player.bounds = Rect(self.bounds.w / 2 - IMAGE_WIDTH / 2, BLOCK_HEIGHT + BLOCK_DEPTH / 2, IMAGE_WIDTH, IMAGE_HEIGHT)
        self.player_max_y = self.bounds.h * 0.6

    def draw(self):
        self.game_loop()
        background(0.40, 0.80, 1.00)
        for sprite in self.scenery:
            sprite.draw()
        self.player.draw()
        self.draw_text()

    def touch_began(self, touch):
        if self.game_state == GAME_WAITING:
            self.game_state = GAME_PLAYING
            self.player.velocity = PLAYER_INITIAL_BOUNCE
        elif self.game_state == GAME_DEAD:
            self.setup()

run(MyScene(), PORTRAIT)

bashedcrab

Thanks Sebastian - that looks fantastic. I have updated the Gist with your clouds.

due3die

@bashedcrab

I am really enjoying your games you so awesome...

Sebastian

@bashedcrab Nice! :D

techteej

I added some sounds to the game. https://github.com/tjferry14/Cloud-Jump-2

ccc

I could use some PIL graphics assistance... The clouds in the app are beautiful but when I tried to port them to PIL they look ugly. :-( Would it be possible for someone to make the PIL image-based clouds look as beautiful as in the app?

techteej

Maybe @Sebastian can help

brumm

@ccc: Not perfect, but I think you ...

import Image, ImageDraw, random, scene

def generate_shapes(num_circles):
    shapes = []
    for i in xrange(num_circles):
        x = i * 20 - ((num_circles/2)*30)   #range: -74 .. +40
        y = (random.random()-0.5) * 30      #range: -15 .. +15
        x += 75  # this is a hack!
        y += 20  # this is a hack!
        rad = random.randint(50, 100)       #range: +50 .. +100
        shapes.append([x, y, rad])
    return shapes

def cloud_maker():
    num_circles = 5                         #range: 4..5 => I prefer 5 :)
    #image_size = ((num_circles + 1) * 30, 60)
    image_size = (214, 140)
    theImage = Image.new('RGBA', image_size) #, 'pink')
    draw = ImageDraw.Draw(theImage)

    circles = generate_shapes(num_circles)
    for i in circles:
        bbox = (i[1] - 5, i[0], i[2], i[2])     # x/y swap      
        draw.ellipse(bbox, fill='rgb(90%,90%,90%)')
    for i in circles:
        bbox = (i[1] + 5, i[0], i[2], i[2])     # x/y swap
        draw.ellipse(bbox, fill='white')

    del draw
    return theImage

class Cloud(scene.Layer):
    def __init__(self, parent = None):
        cloud_image = cloud_maker()
        super(self.__class__, self).__init__(scene.Rect(*cloud_image.getbbox()))
        if parent:
            parent.add_layer(self)
        self.image = scene.load_pil_image(cloud_image)

class MyScene(scene.Scene):
    def __init__(self):
        scene.run(self)

    def setup(self):
        self.cloud = Cloud(self)
        self.cloud.frame.x = self.bounds.w * 0.8    # x/y swap
        self.cloud.frame.y = 0

    def draw(self):
        scene.background(0.40, 0.80, 1.00)
        self.root_layer.update(self.dt)
        self.root_layer.draw()
        self.cloud.frame.y -= 1                     # x/y swap
        if not self.bounds.intersects(self.cloud.frame):
            del self.cloud  # whack the old cloud
            self.setup()    # and create a new one

MyScene()

ccc

@brumm. Thanks for this but the PIL-based clouds are still not as beautiful as the ones in the game...

Three key attributes of the game clouds are missing:

  1. The long axis is generally the y axis instead if the x axis (this makes clouds more inviting to land on)
  2. The grey (silver!) lining is on the underside of the clouds (this gives them shading, depth, and believability)
  3. They are not as big and beautiful and believable as the original clouds.

My hacks at this problem have been fruitless.

Sebastian

I think I'm getting there! The ImageDraw.ImageDraw.ellipse function doesn't use a width and height value in its bounding box, but rather x and y values. So maybe something like this?

import Image, ImageDraw, random, scene

def generate_shapes(num_circles):
    shapes = []
    for i in xrange(num_circles):
        x = (i * 20 - ((num_circles/2)*30))+90
        y = ((random.random()-0.5) * 30)+15
        rad = random.randint(50, 100)
        shapes.append([x, y, rad])
    return shapes

def cloud_maker():
    num_circles = random.randint(5, 6)
    image_size = (220, 140)
    theImage = Image.new('RGBA', image_size)
    draw = ImageDraw.Draw(theImage)

    circles = generate_shapes(num_circles)
    for i in circles:
        r = i[2]
        bbox = (i[0], 40-i[1], i[0]+r, 40-i[1]+r)
        draw.ellipse(bbox, fill='rgb(90%,90%,90%)')
    for i in circles:
        r = i[2]
        bbox = (i[0], 40-i[1]-10, i[0]+r, 40-i[1]+r-10)
        draw.ellipse(bbox, fill='white')

    del draw
    return theImage

class Cloud(scene.Layer):
    def __init__(self, parent = None):
        cloud_image = cloud_maker()
        super(self.__class__, self).__init__(scene.Rect(*cloud_image.getbbox()))
        if parent:
            parent.add_layer(self)
        self.image = scene.load_pil_image(cloud_image)

class MyScene(scene.Scene):

    def setup(self):
        self.cloud = Cloud(self)
        self.cloud.frame.x = self.bounds.w * 0.5
        self.cloud.frame.y = self.bounds.h*0.8

    def draw(self):
        scene.background(0.40, 0.80, 1.00)
        self.root_layer.update(self.dt)
        self.root_layer.draw()
        scene.rect(*self.cloud.frame)

    def touch_began(self, touch):
        self.root_layer.remove_layer(self.cloud)
        self.cloud = Cloud(self)
        self.cloud.frame.x = self.bounds.w * 0.5
        self.cloud.frame.y = self.bounds.h*0.8

scene.run(MyScene())
JonB

PIL coordinate system is also flipped in y, so you need to use image height minus y.

Scene allowed you to draw outside of the frame, PIL does not. So you need to add 60/15 respectively so the cords are Zero based. Image size should be 200 wide (5th cloud starts at 80, and is 100 wide), and 140 tall (0 to 30 random height, plus 10 for shadow offset).
Then top would be h-i[1], bottom would be h-i[1]-i[2], and you would subtract 10 for both to shift gray cloud down.

Sebastian

@JonB Thanks! I edited my post above. It seems to work ok now ;)

ccc

You guys are awesome. Thanks much.

ccc

OK guys... One issue remains :-(

The image frame is larger than the cloud itself. See: https://github.com/tjferry14/Cloud-Jump-2/issues/37

Sebastian

I found a solution that may work. @ccc What do you think?

import Image, ImageDraw, random, scene
import numpy as np

class Cloud(scene.Layer):
    def __init__(self, parent = None):
        cloud_image = self.create_image()
        super(self.__class__, self).__init__(scene.Rect(*cloud_image.getbbox()))
        if parent:
            parent.add_layer(self)
        self.image = scene.load_pil_image(cloud_image)

    def generate_shapes(self, num_circles):
        shapes = []
        for i in xrange(num_circles):
            x = (i * 20 - ((num_circles/2)*30))+90
            y = ((random.random()-0.5) * 30)+15
            rad = random.randint(50, 100)
            shapes.append([x, y, rad])
        return shapes

    # found on 'http://stackoverflow.com/questions/14211340/automatically-cropping-an-image-with-python-pil'
    def crop_image(self, img):
        image_data = np.asarray(img)
        image_data_bw = image_data.max(axis=2)
        non_empty_columns = np.where(image_data_bw.max(axis=0)>0)[0]
        non_empty_rows = np.where(image_data_bw.max(axis=1)>0)[0]
        cropBox = (min(non_empty_rows), max(non_empty_rows), min(non_empty_columns), max(non_empty_columns))
        image_data_new = image_data[cropBox[0]:cropBox[1]+1, cropBox[2]:cropBox[3]+1, :]
        img = Image.fromarray(image_data_new)
        return img

    def create_image(self):
        num_circles = random.randint(5, 6)
        image_size = (220, 140)
        theImage = Image.new('RGBA', image_size)
        draw = ImageDraw.Draw(theImage)

        circles = self.generate_shapes(num_circles)
        for i in circles:
            r = i[2]
            bbox = (i[0], 40-i[1], i[0]+r, 40-i[1]+r)
            draw.ellipse(bbox, fill='rgb(90%,90%,90%)')
        for i in circles:
            r = i[2]
            bbox = (i[0], 40-i[1]-10, i[0]+r, 40-i[1]+r-10)
            draw.ellipse(bbox, fill='white')


        del draw
        return self.crop_image(theImage)

class MyScene(scene.Scene):

    def setup(self):
        self.cloud = Cloud(self)
        self.cloud.frame.x = self.bounds.w * 0.5
        self.cloud.frame.y = self.bounds.h*0.8

    def draw(self):
        scene.background(0.40, 0.80, 1.00)
        scene.fill(0,0,0)
        scene.rect(*self.cloud.frame)
        self.root_layer.update(self.dt)
        self.root_layer.draw()

    def touch_began(self, touch):
        self.root_layer.remove_layer(self.cloud)
        self.cloud = Cloud(self)
        self.cloud.frame.x = self.bounds.w * 0.5
        self.cloud.frame.y = self.bounds.h*0.8

scene.run(MyScene())
techteej

@Sebastian thank you so much for your help with this project. Will add you to the contributors list.

Sebastian

@techteej No worries! That's what I love about this forum; you help out if you can, and in return people will help you when you need it :)

ccc

A study on player death was created for those interested to helps us give the game an arcade-like feel. A great solution would close out issue #9. https://github.com/tjferry14/Cloud-Jump-2/blob/master/etude_on_player_death.py

techteej

Link updated from ccc's post: https://github.com/tjferry14/Cloud-Jump-2

techteej

Any animation experts out there with scene that can help us out?

JonsonAbigaile

Can hack or not?
After a period of learning I was able to hack this game with Lucky Patcher application. Please use Lucky Patcher Playstation with many features from this address: https://luckypatcherofficial.com/download/lucky-patcher-on-playstation/

ccc

I just made a from six import StringIO change to the code at https://github.com/tjferry14/Cloud-Jump-2 to get the Travis CI tests to pass. I have not run this in a long time so if you find that other changes are needed, please open a pull request.