Page 1 of 1

[CODE] Color Lines

Posted: Sat Aug 14, 2021 8:31 pm
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 53 times

Re: [CODE] Color Lines

Posted: Tue Aug 17, 2021 9:13 pm
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.

Re: [CODE] Color Lines

Posted: Sat Dec 04, 2021 12:08 pm
by plastiekk
Well explained code, I learned a lot from it (Renpy related). Thanks for sharing @Alex!