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