[CODE] Color Lines

A place for Ren'Py tutorials and reusable Ren'Py code.
Forum rules
Do not post questions here!

This forum is for example code you want to show other people. Ren'Py questions should be asked in the Ren'Py Questions and Announcements forum.
Post Reply
Message
Author
User avatar
Alex
Lemma-Class Veteran
Posts: 3090
Joined: Fri Dec 11, 2009 5:25 pm
Contact:

[CODE] Color Lines

#1 Post by Alex »

lines.png
Classic logic game - move balls on the board to form straight lines, 5 or more balls in a line are removed.

Code: Select all

# The script of the game goes in this file.

####
#
# Color Lines
#
####


####
#
# Functions used in game.
#

init python:
    
    def new_game():
        '''
        This function creates game board, puts the first set of balls on it
        and sets default values for game variables.
        '''
        
        global game_map, game_map_width, game_map_height, start, finish, path_to_go, ball_pos, reachable_cells_list, can_click, current_balls_list, balls_max_index, balls_next_amount, balls_next, balls_number, game_score, got_line
        
        balls_max_index = len(current_balls_list) - 1
        balls_number = 0

        # will try to create an empty game board and place balls on it,
        # if some balls formed the line and was removed will try it again,
        # after 100 attempts will throw an error
        counter = 0
        while balls_number < balls_next_amount and balls_number < (game_map_height * game_map_width):
            counter += 1
            if counter > 100:
                renpy.notify("Error. Change game settings.")
                return None
                
            game_map = []

            for row in range(game_map_height):
                game_map_row = []
                for cell in range(game_map_width):
                    game_map_row.append(0) # 0 represents an empty cell
                game_map.append(game_map_row)
                
            balls_next = []
            get_next_balls()
            place_ball(balls_next)
            set_balls_to_remove()
            if got_line:
                remove_balls()
            balls_next = []
            get_next_balls()
            
        start = None
        finish = None
        path_to_go = []
        ball_pos = None
        reachable_cells_list = []
        can_click = False
        game_score = 0
                
    
    def make_graph(map, start):
        '''
        Function to create traveling graph.
        map - should be a game_map (two dimensional list).
        start - position of ball to move in map (a tuple (x, y)).
        Returns a traveling graph. All nodes and neighbours are tuples (x, y),
        that are coordinates of cells in a map.
        
        credits: https://www.redblobgames.com/pathfinding/grids/graphs.html#grids
        '''
        
        graph = {}
        all_nodes = [start] # put start position in a graph
        
        for y in range(len(map)):
            for x in range(len(map[0])):
                # If cell got value 0 - it is free, otherwise it occupied by ball.
                if map[y][x] == 0:
                    all_nodes.append((x, y))
                    
        dirs = [[1, 0], [0, 1], [-1, 0], [0, -1]]            
        for node in all_nodes:
            neighbors = []
            x, y = node
            for dir in dirs:
                neighbor = (x + dir[0], y + dir[1])
                if neighbor in all_nodes:
                    neighbors.append(neighbor)
                    
            graph[node] = neighbors
            
        return graph
        
    def find_the_path(map, start, finish):
        '''
        Function to create path from start to finish. Path is a list of tuples (x, y)
        that are coordinates of cells in map.
        map - should be a game_map (two dimensional list).
        start - position of ball to move in map (a tuple (x, y)).
        finish - goal position of ball to move in map (a tuple (x, y)).
        Returns path - a list of cells to visit (from goal cell to the one next to start cell),
        so later cells from this list could be popped.
        
        credits: https://www.redblobgames.com/pathfinding/tower-defense/
        '''
        
        # create graph out of current state of map
        graph = make_graph(map, start)
        
        # for each node in graph (cell of game map) sets the node from where
        # one came from
        path = []
        frontier = []
        came_from = {}
        
        frontier.append(start)
        came_from[start] = None
        
        while frontier:
            current = frontier.pop(0)
            # stop searching if we got finish cell
            if current == finish:
                break
                
            for next in graph[current]:
                if next not in came_from:
                    frontier.append(next)
                    came_from[next] = current
                    
        # you shouldn't see this notify
        if finish not in came_from:
            renpy.notify("No path")
            return []
            
        # create path from finish cell to cell next to start
        else:
            current = finish
            while current != start:
                path.append(current)
                current = came_from[current]
                
            return path
            
            
    def get_reachable_cells(map, start):
        '''
        Function to get all reachable cells on game map.
        map - should be a game_map (two dimensional list).
        start - position of ball to move in map (a tuple (x, y)).
        Returns a list of tuples (x, y) that are coordinates of cells in a map
        where ball from "start" position could be moved to.
        
        credits: https://www.redblobgames.com/pathfinding/tower-defense/
        '''
        
        # create graph out of current state of map
        graph = make_graph(map, start)
        
        reachable_cells_list = []
        frontier = []
        came_from = {}
        
        frontier.append(start)
        came_from[start] = None
        
        while frontier:
            current = frontier.pop(0)
                
            for next in graph[current]:
                if next not in came_from:
                    frontier.append(next)
                    came_from[next] = current
                    
                    reachable_cells_list.append(next)
                    
        return reachable_cells_list
        
        
    def get_free_cell(map):
        '''
        Function to get a free cell on map (not occupied by ball).
        map - should be a game_map (two dimensional list).
        Returns a tuple (x, y) that is coordinates of cells in a map.
        '''
        
        free_cells_list = []
        
        for y in range(len(map)):
            for x in range(len(map[0])):
                if map[y][x] == 0:
                    free_cells_list.append((x, y))
                    
        free_cell = renpy.random.choice(free_cells_list)
        
        return free_cell
        
        
    def get_next_balls():
        '''
        Function to get a list of balls to spawn.
        Fills the balls_next list by numeric indexes of balls in the current_balls_list.
        '''
        
        global balls_max_index, balls_next_amount, balls_next
        
        for i in range(balls_next_amount):
            balls_next.append( renpy.random.randint(1, balls_max_index) )
            
            
    def place_ball(balls):
        '''
        Function to place balls onto game board (game map). Balls placed all at once.
        After all balls placed, counts the number of balls on game board.
        balls - a list of numeric indexes of balls in the current_balls_list,
        "balls" list can have a single item.
        '''
        
        global game_map
        
        for ball in balls:
            x, y = get_free_cell(game_map)
            game_map[y][x] = ball
            
        count_balls_on_board()


    def set_balls_to_remove():
        '''
        Function to mark balls to remove from game board.
        '''
        
        global game_map, game_map_width, game_map_height, balls_to_remove, got_line
        
        # the list is the same size as game_map list and filled with 0's
        balls_to_remove = []

        for row in game_map:
            balls_row = []
            for cell in row:
                balls_row.append(0)
            balls_to_remove.append(balls_row)
            
        # let's iterate through the game_map list and mark balls that are form lines -
        # in a balls_to_remove list set cells with coordinates of that balls
        # to 1's
        for y in range(game_map_height):
            for x in range(game_map_width):
                if game_map[y][x] > 0: # if cell is not empty
                    # set possible directions to check for a line
                    directions = []
                    if x <= (game_map_width - line_min_length):
                        directions.append((1, 0)) # to the right
                    if y <= (game_map_height - line_min_length):
                        directions.append((0, 1)) # down
                    if x <= (game_map_width - line_min_length) and y <= (game_map_height - line_min_length):
                        directions.append((1, 1)) # right-down diagonale
                    if x >= (line_min_length - 1) and y <= (game_map_height - line_min_length):
                        directions.append((-1, 1)) # left-down diagonale
                        
                    for dir in directions:
                        dx, dy = dir
                        counter = 1 # current ball is already in the line
                        # check if all the next balls in a line are the same as current
                        for z in range(1, line_min_length):
                            if game_map[y][x] == game_map[y + dy*z][x + dx*z]:
                                counter += 1
                        if counter == line_min_length:
                            got_line = True # we got line of balls
                            # set apropriate cells in balls_to_remove list to 1
                            for z in range(0, line_min_length):
                                balls_to_remove[y + dy*z][x + dx*z] = 1
                    
        
    def remove_balls():
        '''
        Function to remove marked balls from game board and add scores for them.
        After all is done, counts the number of balls on game board.
        '''
        
        global game_map, balls_to_remove, ball_cost, game_score, got_line
        
        for y in range(game_map_height):
            for x in range(game_map_width):
                if balls_to_remove[y][x] == 1:
                    game_map[y][x] = 0
                    game_score += ball_cost
        
        got_line = False # sets the flag variable to False
        balls_to_remove = []
                    
        count_balls_on_board()
        
        
    def count_balls_on_board():
        '''
        Function to count balls on game board.
        '''
        
        global game_map, balls_number
        
        balls_number = 0

        for row in game_map:
            for cell in row:
                if cell > 0:
                    balls_number += 1
                    
                    
    def tap_sound_func(trans, st, at):
        '''
        Function to play sound of ball bouncing. Used in bounce_tr transform.
        tap_sound - a sound file to play.
        '''
        
        renpy.sound.play(tap_sound)
        return None
#
####

####
#
# Variables to describe the game state.
#

# game map is a two dimensional list, it will be created
# using game_map_width and game_map_height variables;
# value 0 represents an empty cell, other values - are indexes of balls in a list
default game_map = []

# the size of the game board
default game_map_width = 9
default game_map_height = 9

# align of game board onscreen
default game_map_align = (0.25, 0.5)

# the size of game board cell
# (is used for 'fixed')
default game_cell_width = 75
default game_cell_height = 75

default start = None # None or tuple (x, y) coordinates of cell in game_map
default finish = None # None or tuple (x, y) coordinates of cell in game_map
default path_to_go = [] # list of cells - tuples (x, y) coordinates of cell in game_map
default ball_pos = None # None or tuple (x, y) coordinates of cell in game_map
default reachable_cells_list = [] # list of cells - tuples (x, y) coordinates of cell in game_map
default can_click = False # flag to disable / enable player's interactions
default got_line = False # flag to indicate if there are balls to remove
default line_min_length = 5 # minimum length of balls line

# list of all possible balls in game
default balls_list = ['red', 'green', 'blue', 'purple', 'cyan', 'gold', 'dark_red']

# list of balls in current game,
# should have 'None' as first item - value 0 in game_map represents an empty cell,
# value different from 0 - is an index of ball in current_balls_list
default current_balls_list = [None, 'red', 'green', 'blue', 'purple', 'cyan', 'gold', 'dark_red']

default balls_max_index = len(current_balls_list) - 1 # maximum value of a ball's index
default balls_to_remove = [] # list of balls to remove  - tuples (x, y) coordinates of cell in game_map
default balls_next = [] # list of balls to spawn
default balls_next_amount = 3 # number of balls to spawn
default ball_cost = 2 # points got for each removed ball
default balls_number = 0 # number of balls on game board

default game_score = 0 # player's score

default ball_anim_time = 0.5 # ball's remove animation time
default time_delay_move = 0.05 # ball's move delay
default time_delay_spawn = 0.3 # delay between balls while spawn
default time_delay_turn = 0.1 # delay batween actions

define tap_sound = "tap.ogg" # sound to play - should include the path from 'game' folder

#
####

####
#
# Images and transforms.
#

image tile:
    "images/tile.png"
    tile_scale_down_tr
    
image shadow:
    "images/shadow.png"
    ball_scale_down_tr
    
image grey:
    "images/grey_ball.png"
    ball_scale_down_tr
    
image blue:
    "images/blue_ball.png"
    ball_scale_down_tr
    
image green:
    "images/green_ball.png"
    ball_scale_down_tr
    
image red:
    "images/red_ball.png"
    ball_scale_down_tr
    
image purple:
    "images/purple_ball.png"
    ball_scale_down_tr
    
image cyan:
    "images/cyan_ball.png"
    ball_scale_down_tr
    
image gold:
    "images/gold_ball.png"
    ball_scale_down_tr
    
image dark_red:
    "images/dark_red_ball.png"
    ball_scale_down_tr
    
    
# if ball image is bigger than cell_size, let's shrink it down
transform ball_scale_down_tr:
    zoom 0.4
    
# if cell image is bigger than cell_size, let's shrink it down
transform tile_scale_down_tr:
    zoom 0.5

# selected ball's transform
transform bounce_tr(t = 0.4, dist = -20):
    yoffset 0 yzoom 1.0
    linear t/2.0 yzoom 0.9
    block:
        linear t yoffset dist yzoom 1.0
        linear t yoffset 0
        function tap_sound_func # ball's sound
        linear t/2.0 yzoom 0.9
        linear t/2 yzoom 1.0
        repeat
    
# transform for removing balls
transform blast_out_tr(t = ball_anim_time):
    zoom 1.0 alpha 1.0
    linear t zoom 1.1 alpha 0.0

#
####

####
#
# Screens.
#

# screen to set game's parameters
screen game_settings_scr():
    frame:
        align(0.5, 0.2)
        padding (20, 20, 20, 20)
        vbox:
            spacing 10
            hbox:
                text "Board size:"
                textbutton "- " action SetVariable('game_map_width', max(5, game_map_width - 1))
                text "[game_map_width]"
                textbutton " +" action SetVariable('game_map_width', min(9, game_map_width + 1))
                text " X "
                textbutton "- " action SetVariable('game_map_height', max(5, game_map_height - 1))
                text "[game_map_height]"
                textbutton " +" action SetVariable('game_map_height', min(9, game_map_height + 1))

            hbox:
                text "Number of balls to spawn:"
                for i in range(3, 10):
                    textbutton "[i]" action SetVariable ('balls_next_amount', i)
                    
            hbox:
                text "Colors:" yalign 0.5
                for ball in balls_list:
                    button:
                        if ball not in current_balls_list:
                            add 'grey' align(0.5, 0.5)
                        else:
                            add ball align(0.5, 0.5)
                            
                        if len(current_balls_list) > 2:
                            action ToggleSetMembership(current_balls_list, ball)
                        else:
                            action AddToSet(current_balls_list, ball)

            hbox:
                text "Line length:"
                for i in range(3, 8):
                    textbutton "[i]" action SetVariable ('line_min_length', i)
            null
            textbutton "Done" xalign 0.5 action Return()
            null
                    
        
# main game screen
screen lines_game_scr():
    
    # game stats
    frame:
        align(0.95, 0.05)
        ysize int(config.screen_height * 0.5)
        vbox:
            xsize int(config.screen_width * 0.2)
            spacing 5
            text 'Score - {}'.format(game_score)
            text 'Balls - {}'.format(balls_number)
            null
            hbox:
                box_wrap True
                spacing 2
                for ball in balls_next:
                    add current_balls_list[ball]
                null height game_cell_height

    
    # game board
    frame:
        background None
        align game_map_align
        grid game_map_width game_map_height:
            for y in range (game_map_height):
                for x in range (game_map_width):
                    if game_map[y][x] == 0: # empty cell
                        # if start is set (ball selected) and this cell
                        # is reachable - make it a button
                        if start and (x, y) in reachable_cells_list:
                            button:
                                add 'tile'
                                padding (0, 0)
                                sensitive can_click
                                action [SetVariable('finish', (x, y)), Return('go')]
                                
                        else: # otherwise - just an image
                            add 'tile'
                            
                    # if cell is not empty
                    else:
                        fixed:
                            xysize(game_cell_width, game_cell_height)
                            add 'tile' # cell's background
                            
                            # ball's shadow
                            # if ball marked to remove
                            if got_line and balls_to_remove[y][x] == 1:
                                add 'shadow' align (0.5, 0.5) at blast_out_tr(ball_anim_time)
                            # and if not
                            else:
                                add 'shadow' align (0.5, 0.5)
                            
                            # the ball
                            button:
                                align (0.5, 0.5)
                                padding (0, 0)
                                # value of the cell is an index of ball in a current_balls_list
                                add current_balls_list[ game_map[y][x] ]
                                # if ball marked to remove
                                if got_line and balls_to_remove[y][x] == 1:
                                    at blast_out_tr(ball_anim_time)
                                sensitive can_click
                                # if ball selected show it at bounce transform
                                # and unselect it on click
                                if start == (x, y):
                                    action [SetVariable('start', None), Return('start_reset')]
                                    at bounce_tr
                                # otherwise - select ball
                                else:
                                    action [SetVariable('start', (x, y)), Return('start_set')]



# The game starts here.

label start:
    "..."
    call lines_game_lbl # calling the game label
    "The end."
    jump start
    
label lines_game_lbl: # game label
    
    "Let's play."
    window hide
    $ quick_menu = False
    
    # you can set game parameters manually
    # $ game_map_width = 9
    # $ game_map_height = 9
    # $ current_balls_list = [None, 'red', 'green', 'blue', 'purple', 'cyan', 'gold', 'dark_red']
    # $ line_min_length = 5
    
    # or use a screen for game settings
    call screen game_settings_scr
    
    $ new_game() # sets the new game
    
    # now show the game screen
    show screen lines_game_scr
    
    label lines_game_loop: # game loop
        
        $ can_click = True # now player can interact the game
        $ res = ui.interact() # got result of user interaction
        $ can_click = False # prevent further interactions
        
        # evaluate the result of player's interaction
        if res == 'start_set':
            $ ball_pos = start # got the position of selected ball
            # get the list of reachable cells for selected ball
            $ reachable_cells_list = get_reachable_cells(game_map, start)
            jump lines_game_loop
            
        elif res == 'start_reset':
            # drop the values
            $ ball_pos = None
            $ reachable_cells_list = []
            jump lines_game_loop
            
        elif res == 'go':
            $ x, y = ball_pos # coordinates of selected ball
            $ ball = game_map[y][x] # get selected ball (the value of game_map cell)
            $ path_to_go = find_the_path(game_map, start, finish) # get the path to go

            # moving the ball
            while path_to_go:
                $ renpy.pause(time_delay_move) # time delay between ball's moves
                $ game_map[y][x] = 0 # remove ball from current position
                $ ball_pos = path_to_go.pop() # got new position from a list
                $ x, y = ball_pos
                $ game_map[y][x] = ball # put selected ball at new coordinates
                play sound tap_sound # play sound of ball moving
                
            # drop values
            $ start = None
            $ finish = None
            $ path_to_go = []
            $ ball_pos = None
            $ reachable_cells_list = []
            $ renpy.pause(time_delay_turn)
            
            $ set_balls_to_remove() # check result of moving

            # if got line of balls to remove
            if got_line:
                $ renpy.pause(ball_anim_time) # time delay to show animation
                $ remove_balls() # remove balls from game board
                
                if balls_number == 0: # if no balls left
                    jump game_win_lbl
                jump lines_game_loop # otherwise player can move next ball
            
            # if no balls were removed let's place new balls from balls_next list
            while balls_next:
                # if game board is not full
                if balls_number < (game_map_width * game_map_height):
                    $ ball = balls_next.pop() # got one ball from a list
                    $ place_ball([ball]) # function need a list as argument, so [ball]
                    $ renpy.pause(time_delay_spawn) # time delay between balls appearing
                
                # if game board is full
                else:
                    jump game_over_lbl
                    
            $ set_balls_to_remove() # check result of placing new balls

            # if got line of balls to remove
            if got_line:
                $ renpy.pause(ball_anim_time) # time delay to show animation
                $ remove_balls() # remove balls from game board
            
            $ renpy.pause(time_delay_turn) # time delay before next turn
            
            # if no balls left
            if balls_number == 0:
                jump game_win_lbl
                
            # if game board is full
            elif balls_number == (game_map_width * game_map_height):
                jump game_over_lbl
            
            $ get_next_balls() # get some balls to spawn
    
            jump lines_game_loop
        

label game_over_lbl:
    "Game over."
    hide screen lines_game_scr
    return # returns from game label since we've called it
    
label game_win_lbl:
    "No balls left."
    hide screen lines_game_scr
    return # returns from game label since we've called it
Lines.rar
(195.14 KiB) Downloaded 88 times

User avatar
bonnie_641
Regular
Posts: 133
Joined: Sat Jan 13, 2018 10:57 pm
Projects: Código C.O.C.I.N.A.
Deviantart: rubymoonlily
Contact:

Re: [CODE] Color Lines

#2 Post by bonnie_641 »

Alex wrote: Sat Aug 14, 2021 8:31 pm lines.png
Classic logic game - move balls on the board to form straight lines, 5 or more balls in a line are removed.
I just saw it :oops:
I really liked this game. Thank you very much for sharing.
I speak and write in Spanish. I use an English-Spanish translator to express myself in this forum. If I make any mistakes, please forgive me.
I try my best to give an answer according to your question. :wink:

User avatar
plastiekk
Regular
Posts: 112
Joined: Wed Sep 29, 2021 4:08 am
Contact:

Re: [CODE] Color Lines

#3 Post by plastiekk »

Well explained code, I learned a lot from it (Renpy related). Thanks for sharing @Alex!
Why on earth did I put the bread in the fridge?

Post Reply

Who is online

Users browsing this forum: No registered users