--- /dev/null
+#!/usr/bin/python3
+# (c) Copyright 2013 TJ <hacker@iam.tj>
+# Licensed on the terms of the GNU General Public License version 3 (see COPYING)
+#
+# 2D platform scrolling game that teaches binary logic
+
+import sys, os, time, random, pygame
+import engine
+
+# Use conditionals in the code to print useful information to the console using the form:
+# if self.debug: engine.debug_pr("some_var=%d, another_var=%s" % (some_var, another_var), somevalue, another_value)
+
+class Base2Runner:
+ background_image = None
+ stepping = 8 # pixels scrolled each flip
+ delay = 0.01 # main loop sleep (100 milliseconds)
+ background_colour = (0, 0, 0)
+ background_image = None
+ icon_image = None
+ flags = pygame.OPENGL & pygame.DOUBLEBUF
+ depth = 0
+ screen = None # the pygame Screen for the game window
+
+ def __init__(self, width, height, title, resources={}, debug=False):
+ pygame.init()
+ self.debug = debug
+ self.myGame = engine.Game(width, height, title, resources, debug=self.debug)
+ self.background_colour = (20, 20, 64)
+ if 'icon' in self.myGame.resources:
+ if self.debug: engine.debug_pr("pygame.image.get_extended()=%r" % pygame.image.get_extended())
+ # pygame.display.set_mode((1,1), self.flags, self.depth) # needed simply to use convert_alpha() on icon
+ self.icon_image = pygame.image.load(self.myGame.resources['icon']) #.convert_alpha()
+ # pygame.display.quit() # need to close display now that icon has been converted
+ # pygame.display.init() # and now re-initialise the display!
+ pygame.display.set_icon(self.icon_image)
+
+ def play(self):
+ self.screen = pygame.display.set_mode(self.myGame.resolution, self.flags, self.depth)
+ self.screen.fill(self.background_colour)
+ pygame.display.set_caption(self.myGame.config['title'])
+ resolution = self.screen.get_size()
+
+ # background image
+ self.background_image = engine.SeamlessBackground(self.myGame.resources['background'], debug=self.debug)
+ self.background_image.draw(self.screen, -1, 0) # -1 flags initial background drawing
+ pygame.display.update()
+
+ # scrolling object
+ blip = pygame.Surface((abs(self.stepping), abs(self.stepping)), pygame.HWSURFACE)
+ blip_colour = (128, 128, 128)
+ blip.fill(blip_colour)
+
+ # starting position
+ y = resolution[1] / 2
+
+ if self.myGame.config['sound'] == 1:
+ self.myGame.soundtrack.playlist(self.myGame.resources['soundtrack'])
+
+ self.myGame.config['auto_scroll'] = False
+ movement = 0 # represents number of pixels, and direction, of next scroll
+ x_pos = 0 # coordinate of left side of viewport
+
+ self.myGame.play = True # start the game
+ while (self.myGame.play):
+
+ if not self.myGame.paused:
+ # update game state
+ if x_pos + movement >= 0:
+ x_pos += movement
+ else:
+ movement = 0
+
+ # change colour randomly
+ d_red = random.randrange(-1,2) *8
+ d_green = random.randrange(-1,2) *8
+ d_blue = random.randrange(-1,2) *8
+ # build a modified colour tuple, ensuring all values are valid
+ blip_colour = tuple( b+d if b+d < 256 and b+d >=0 else b for b, d in zip(blip_colour, (d_red, d_green, d_blue)) )
+ blip.fill(blip_colour)
+
+ # change blip position up or down randomly by one step
+ dy = random.randrange(-1,2) * abs(self.stepping)
+ if((y + dy) < resolution[1] and (y + dy) >= 0):
+ # TODO: no longer need to redraw background colour if using a background image
+ # screen.fill(background_colour, pygame.Rect(resolution[0] - abs(stepping), y, abs(stepping), abs(stepping) ))
+ y += dy
+
+ # redraw the display
+ self.screen.scroll(-movement, 0)
+ self.background_image.draw(self.screen, x_pos, movement)
+ self.screen.blit(blip, (resolution[0] - abs(self.stepping), y))
+ pygame.display.update()
+
+ # manage the frame-rate
+ time.sleep(self.delay)
+
+ # process events
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT or (event.type == pygame.KEYUP and event.key == pygame.K_q):
+ self.myGame.play = False
+ if event.type == pygame.ACTIVEEVENT:
+ if self.debug: engine.debug_pr("active=%d, pygame.ACTIVEEVENT:" % pygame.display.get_active(), event)
+ if event.type == pygame.KEYUP:
+ k = event.key
+ if k == pygame.K_p:
+ time.sleep(10)
+ elif k == pygame.K_7:
+ self.myGame.soundtrack.pause()
+ elif k == pygame.K_l:
+ self.myGame.config['auto_scroll'] = True if self.myGame.config['auto_scroll'] == False else False
+
+ if event.type == pygame.USEREVENT:
+ if self.debug: engine.debug_pr("pygame.USEREVENT received; calling playlist_next()")
+ self.myGame.soundtrack.playlist_next()
+
+ if not self.myGame.config['auto_scroll']:
+ # reset if not auto-scrolling
+ movement = 0
+
+ # detect which keys are held down
+ keys_pressed = pygame.key.get_pressed()
+ if sum(keys_pressed):
+ if keys_pressed[pygame.K_6]:
+ # volume down
+ self.myGame.soundtrack.volume_down()
+ elif keys_pressed[pygame.K_8]:
+ # volume up
+ self.myGame.soundtrack.volume_up()
+ elif keys_pressed[pygame.K_d]:
+ # move right
+ movement = self.stepping
+ elif keys_pressed[pygame.K_a]:
+ # move left
+ movement = -self.stepping
+
+ # end of game
+ self.myGame.soundtrack.fadeout(5000)
+ time.sleep(5)
+ pygame.display.quit()
+
+
+def main(width, height, title, resources={}, debug=False):
+ game = Base2Runner(width, height, title, resources, debug)
+ game.play()
+ exit(0)
+
+if __name__ == '__main__':
+ resources = {}
+ resources['path'] = 'resources'
+ resources['icon'] = 'base2runner-icon.png'
+ resources['background'] = 'binary-1024x1024.jpg'
+ resources['soundtrack'] = 'Music'
+ main(900, 600, "BaseĀ² Runner", resources, debug=True)
--- /dev/null
+#!/usr/bin/python3
+# (c) Copyright 2013 TJ <hacker@iam.tj>
+# Licensed on the terms of the GNU General Public License version 3 (see COPYING)
+#
+# Game engine: Sound channels
+
+import os, random, pygame
+import engine
+
+class Channel:
+ """ Represents an audio output. Can represent a user-created SDL channel or the single
+ Music channel (which can load/play Ogg, MP3 and other audio media). The Music channel
+ can be primed with a playlist which will shuffle and loop.
+ Many methods have dual functionality depending on whether the instance object represents
+ the Music channel or a Sound channel, since those PyGame Classes have some differences in
+ functionality.
+ """
+ # use integers to control volume which avoids float rounding error issues
+ MAX = 100
+ MIN = 0
+ volume_step = 5
+ def __init__(self, volume=1.0, channel_id=0, debug=False):
+ """ Constructor.
+ volume: an SDL/PyGame floating-point with range 0.00 to 1.00. It is stored internally as an
+ integer with range 0 to 100 to avoid rounding errors when stepping the volume.
+ channel_id: -1 == create the single Music channel. 0,1,2,3... one of several Sound channels.
+ debug: enables debug messages written to console
+
+ self.channel == None indicates this is the Music channel.
+ """
+ self.volume = int(volume*100)
+ self.channel_id = 0
+ self.debug = debug
+ self.paused = False
+ if channel_id == -1:
+ self.channel_id = channel_id
+ self.channel = None # flag to show this uses pygame.mixer.music
+ elif channel_id < pygame.mixer.get_num_channels():
+ self.channel_id = channel_id
+ self.channel = pygame.mixer.Channel(self.channel_id)
+
+ def set_volume(self, new_level):
+ """ Ensure the new level is within legal range and alter the mixer level """
+ if new_level >= Channel.MIN and new_level <= Channel.MAX:
+ self.volume = new_level
+ if self.channel == None:
+ pygame.mixer.music.set_volume(self.volume/100)
+ else:
+ self.channel.set_volume(self.volume/100)
+ if self.debug: engine.debug_pr("Channel.set_volume(", self.volume, ")")
+
+ def volume_up(self):
+ """ Increase volume by a single step """
+ if self.volume + self.volume_step <= Channel.MAX:
+ self.set_volume(self.volume + self.volume_step)
+
+ def volume_down(self):
+ """ Decrease volume by a single step """
+ if self.volume - self.volume_step >= Channel.MIN:
+ self.set_volume(self.volume - self.volume_step)
+
+ def get_volume(self):
+ return self.volume
+
+ def queue(self, sound):
+ """ Sound channels can queue one Sound in addition to the currently playing Sound """
+ if self.channel != None:
+ self.channel.queue(sound)
+
+ def get_queue(self):
+ """ Retrieve the queued Sound (if any) """
+ return self.channel.get_queue() if self.channel != None else None
+
+ def fadeout(self, time):
+ """ Fade out the sound nicely
+ time: milliseconds
+ """
+ if self.channel == None:
+ pygame.mixer.music.fadeout(time)
+ else:
+ self.channel.fadeout(time)
+
+ def play(self, sound, loops=0, maxtime=0, fade_ms=0):
+ """ Play a Music track or Sound
+ sound:
+ loops: number of repeats (after initial play - see pyGame API docs)
+ maxtime: seconds to play. 0 == to the end
+ fade_ms: fade-out time in milliseconds
+ """
+ if self.channel != None:
+ # allow 'sound' to be a path string or an object
+ sound = pygame.mixer.Sound(sound)
+ self.channel.play(sound, loops, maxtime, fade_ms)
+ else:
+ pygame.mixer.music.load(sound)
+ pygame.mixer.music.play(loops)
+
+ def pause(self):
+ """ Toggle the play/pause state of the channel """
+ if self.paused:
+ self.paused = False
+ if self.channel == None:
+ pygame.mixer.music.unpause()
+ else:
+ self.channel.unpause()
+ else:
+ self.paused = True
+ if self.channel == None:
+ pygame.mixer.music.pause()
+ else:
+ self.channel.pause()
+
+ def playlist(self, path):
+ """ Create a playlist from a given file-system directory
+ path: absolute or relative path to a directory containing *only* valid media files (Ogg, MP3, etc.)
+
+ See pygame.mixer.music API docs
+ """
+ self.music_path = path
+ # build a list of the filenames in the directory (ignores sub-directories)
+ self.playlist = [ f for f in os.listdir(self.music_path) if os.path.isfile(os.path.join(self.music_path, f)) ]
+ random.shuffle(self.playlist) # randomise the play order
+
+ if self.debug: engine.debug_pr("playlist=", self.playlist)
+
+ self.playlist_index = -1 # flag for 'start from the beginning of list'
+ self.playlist_next() # begin play-back
+ # generate an event when the current track ends
+ pygame.mixer.music.set_endevent(pygame.USEREVENT)
+
+ def playlist_next(self):
+ """ Begin playing the next track in the playlist. Shuffle and loop when it reaches the end of the list. """
+ if self.playlist_index == len(self.playlist)-1:
+ random.shuffle(self.playlist)
+ self.playlist_index = self.playlist_index + 1 if self.playlist_index < len(self.playlist)-1 else 0
+ if self.debug: engine.debug_pr("Channel.playlist_next() = [%d] %s" %(self.playlist_index, self.playlist[self.playlist_index] ))
+
+ pygame.mixer.music.load(os.path.join(self.music_path, self.playlist[self.playlist_index]))
+ pygame.mixer.music.play()
+
--- /dev/null
+#!/usr/bin/python3
+# (c) Copyright 2013 TJ <hacker@iam.tj>
+# Licensed on the terms of the GNU General Public License version 3 (see COPYING)
+#
+# Game engine: game state and configuration
+
+import os.path, pygame, engine
+
+class Game:
+ """ Container for all game state and configuration items """
+ config = dict()
+ resources = None
+
+ def res_path(self, key):
+ """ Create a complete resource path
+ key: the item in the resource dictionary to make path for
+ """
+ if key in self.resources:
+ self.resources[key] = os.path.join(self.resources['path'], self.resources[key])
+ return True
+
+ return False
+
+ def __init__(self, width=0, height=0, title="", resources={}, debug=False):
+ """ Constructor.
+ width: desired width of the game window
+ height: desired height of the game window
+ title: Window caption (title-bar)
+ resources: Dictionary containing resources
+ debug: enable debug messages written to console
+ """
+ self.config.update({'width':width, 'height':height})
+ self.config.update({'title':title})
+ self.resolution = width, height
+ self.debug = debug
+
+ self.resources = resources
+ if not self.resources['path']:
+ self.resources['path'] = ''
+
+ self.res_path('soundtrack')
+ self.res_path('icon')
+ self.res_path('background')
+
+ # default values
+ self.config.update({'sound':1}) # default to playing sound
+ self.play = False # Flag that controls exit from main loop
+ self.paused = False # Flag controlling pause of game play
+ self.soundtrack = engine.Channel(channel_id=-1, debug=self.debug)
+ self.sound_effects = engine.Channel(channel_id=0, debug=self.debug)
+
+ def pause(self):
+ """ Toggle the game's paused state """
+ self.paused = True if self.paused == False else False
+ if self.paused:
+ if self.config['sound']:
+ self.soundtrack.pause()
+ self.sound_effects.pause()
+ else:
+ if self.config['sound']:
+ self.soundtrack.unpause()
+ self.sound_effects.unpause()
--- /dev/null
+#!/usr/bin/python3
+# (c) Copyright 2013 TJ <hacker@iam.tj>
+# Licensed on the terms of the GNU General Public License version 3 (see COPYING)
+#
+# Game engine: scrolling seamless background image manager
+
+import os, time, pygame, engine
+
+class SeamlessBackground:
+ """ The game window has a scrolling background image that is seamless, which allows it to be tiled
+ so that as the viewport scrolls the image appears to be continuous in both directions.
+
+ The game window only scrolls left or right, which means that the background needs to be redrawn
+ at the left or right margins (depending on direction of scroll) or, as a one-off, the entire
+ background image needs drawing at the beginning of the game.
+
+ The seamless background image provided should be larger (wider) than the intended game window to
+ allow the wrapping of the image to work correctly.
+ """
+ image_path = None
+ image = None
+ width = None
+
+ def __init__(self, filename, debug=False):
+ """ Constructor.
+ filename: absolute or relative path to seamless background image file (JPeG, PNG, etc.)
+ """
+ self.debug = debug
+ if os.path.exists(filename):
+ self.image_path = filename
+ self.image = pygame.image.load(self.image_path).convert()
+ self.width = self.image.get_size()[0]
+
+ if self.image == None:
+ print("Error: failed to load image", filename)
+
+ def draw(self, screen, x_pos, step):
+ """ Redraw damaged areas of the game screen.
+ x_pos: -1 == request the full screen be painted, not just the damaged left/right margins. Otherwise,
+ it is the current game-level's left-edge x coordinate.
+ step: the number of pixels, and direction, the screen has just scrolled (negative, 0, or positive)
+ """
+ if self.image != None:
+ # resolution[0] = width, [1] = height
+ resolution = screen.get_size()
+ # adjust x_pos to be x coord of damaged rectangle (left or right side of screen)
+ if step > 0:
+ x_pos += resolution[0] - step
+ elif step < 0:
+ x_pos -= step
+ elif step == 0 and x_pos >= 0:
+ # nothing to do if not drawing the initial background
+ return
+
+ # calculate the offset into the background image such that the scrolled
+ # game window and the background image will match
+
+ # if drawing the background for the first time (x_pos==-1) use the entire image
+ # use modulo to get the offset within the image of the first damaged column
+ bk_x_start = x_pos % self.width if x_pos != -1 else 0
+ bk_rect = pygame.Rect(bk_x_start, 0, resolution[0] if x_pos == -1 else abs(step), resolution[1])
+ # When the image wraps there might not be 'step' columns of pixels available from the right side of
+ # the seamless image. Therefore use an intermediate 'patch' image to build the complete image to be drawn
+ # to the game window
+ patch = pygame.Surface((bk_rect.width, bk_rect.height))
+ patch.blit(self.image, (0, 0), bk_rect)
+
+ remainder = self.width - bk_x_start
+ if remainder < abs(step):
+ # need more pixel columns because the seamless image just wrapped around
+ patch.blit(self.image, (4,0), pygame.Rect(0, 0, remainder, resolution[1]))
+
+ # calculate the left coord in the game window where the new background should be drawn
+ scr_x_dest = resolution[0] - step if step > 0 else 0
+
+ screen.blit(patch, (scr_x_dest, 0))
+ else:
+ print("Warning: SeamlessBackground: No image to draw")
+
--- /dev/null
+#!/usr/bin/python3
+# (c) Copyright 2013 TJ <hacker@iam.tj>
+# Licensed on the terms of the GNU General Public License version 3 (see COPYING)
+
+__all__ = ["Game", "Channel", "SeamlessBackground", "logging"]
+
+"""
+ this code allows us to use the Java packaging convention of one file per class and
+ still have each class exist in the package namespace, rather than in a sub-module
+ namespace.
+
+ E.g: given the following directory structure:
+
+ package/
+ module1.py
+ Class mod1Class
+ module2.py
+ Class mod2Class
+ module3.py
+ Class mod3Class
+
+If our code did:
+
+ import engine
+
+The classes would be accessed using these namespaces:
+ package.module1.mod1Class
+ package.module2.mod2Class
+ package.module3.mod3Class
+
+But we'd prefer to acccess them as:
+ package.mod1Class
+ package.mod2Class
+ package.mod3Class
+
+Therefore, when this package is loaded it reads the __all__ attribute which contains
+a list of the module names to be included in the package namespace. An import statement
+is created dynamically and then executed of the form "from .module_name import *".
+
+The leading "." is required to ensure that the current (package) directory is searched for
+the module file.
+
+This is needed because a static statement "from module_name import *" will not evaluate
+"module_name" as a variable; instead it'll treat it as the literal module of that name.
+"""
+for module_name in __all__:
+ cmd = "from .%s import *" % module_name
+ exec(cmd)
--- /dev/null
+def debug_pr(*args):
+ print("debug:", args)
+
+
--- /dev/null
+[Desktop Entry]
+Encoding=UTF-8
+Name=Link to My Account - Likeboxes
+Type=Link
+URL=http://www.123rf.com/mylightbox.php
+Icon=text-html
+Name[en_GB]=Link to 123RF - Likeboxes