Refactor experimental demo into final state for developing the game
authorTJ <games@iam.tj>
Mon, 11 Nov 2013 19:15:24 +0000 (19:15 +0000)
committerTJ <games@iam.tj>
Mon, 11 Nov 2013 19:15:24 +0000 (19:15 +0000)
20 files changed:
base2-runner.py [new file with mode: 0755]
engine/.Game.py.swp [new file with mode: 0644]
engine/Channel.py [new file with mode: 0755]
engine/Game.py [new file with mode: 0755]
engine/SeamlessBackground.py [new file with mode: 0755]
engine/__init__.py [new file with mode: 0644]
engine/__pycache__/Channel.cpython-33.pyc [new file with mode: 0644]
engine/__pycache__/Game.cpython-33.pyc [new file with mode: 0644]
engine/__pycache__/SeamlessBackground.cpython-33.pyc [new file with mode: 0644]
engine/__pycache__/__init__.cpython-33.pyc [new file with mode: 0644]
engine/__pycache__/logging.cpython-33.pyc [new file with mode: 0644]
engine/logging.py [new file with mode: 0644]
resources/14264121_s-icon.jpg [new file with mode: 0644]
resources/19122178_ml-base2runner.jpg [new file with mode: 0644]
resources/20661978_l-binary.jpg [new file with mode: 0644]
resources/Link to 123RF - Likeboxes.desktop [new file with mode: 0644]
resources/base2runner-icon.png [new file with mode: 0644]
resources/base2runner.jpg [new file with mode: 0644]
resources/binary-1024x1024.jpg [new file with mode: 0644]
resources/binary-2048x2048.jpg [new file with mode: 0644]

diff --git a/base2-runner.py b/base2-runner.py
new file mode 100755 (executable)
index 0000000..091c75b
--- /dev/null
@@ -0,0 +1,153 @@
+#!/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)
diff --git a/engine/.Game.py.swp b/engine/.Game.py.swp
new file mode 100644 (file)
index 0000000..1567f72
Binary files /dev/null and b/engine/.Game.py.swp differ
diff --git a/engine/Channel.py b/engine/Channel.py
new file mode 100755 (executable)
index 0000000..71c6651
--- /dev/null
@@ -0,0 +1,140 @@
+#!/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()
+
diff --git a/engine/Game.py b/engine/Game.py
new file mode 100755 (executable)
index 0000000..38174de
--- /dev/null
@@ -0,0 +1,62 @@
+#!/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()
diff --git a/engine/SeamlessBackground.py b/engine/SeamlessBackground.py
new file mode 100755 (executable)
index 0000000..ebdd66b
--- /dev/null
@@ -0,0 +1,79 @@
+#!/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")
+
diff --git a/engine/__init__.py b/engine/__init__.py
new file mode 100644 (file)
index 0000000..b0fef8a
--- /dev/null
@@ -0,0 +1,48 @@
+#!/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)
diff --git a/engine/__pycache__/Channel.cpython-33.pyc b/engine/__pycache__/Channel.cpython-33.pyc
new file mode 100644 (file)
index 0000000..19a3c3d
Binary files /dev/null and b/engine/__pycache__/Channel.cpython-33.pyc differ
diff --git a/engine/__pycache__/Game.cpython-33.pyc b/engine/__pycache__/Game.cpython-33.pyc
new file mode 100644 (file)
index 0000000..55bcdc9
Binary files /dev/null and b/engine/__pycache__/Game.cpython-33.pyc differ
diff --git a/engine/__pycache__/SeamlessBackground.cpython-33.pyc b/engine/__pycache__/SeamlessBackground.cpython-33.pyc
new file mode 100644 (file)
index 0000000..8cf5920
Binary files /dev/null and b/engine/__pycache__/SeamlessBackground.cpython-33.pyc differ
diff --git a/engine/__pycache__/__init__.cpython-33.pyc b/engine/__pycache__/__init__.cpython-33.pyc
new file mode 100644 (file)
index 0000000..f4b6d83
Binary files /dev/null and b/engine/__pycache__/__init__.cpython-33.pyc differ
diff --git a/engine/__pycache__/logging.cpython-33.pyc b/engine/__pycache__/logging.cpython-33.pyc
new file mode 100644 (file)
index 0000000..dae7184
Binary files /dev/null and b/engine/__pycache__/logging.cpython-33.pyc differ
diff --git a/engine/logging.py b/engine/logging.py
new file mode 100644 (file)
index 0000000..51e0fe7
--- /dev/null
@@ -0,0 +1,4 @@
+def debug_pr(*args):
+ print("debug:", args)
+
+
diff --git a/resources/14264121_s-icon.jpg b/resources/14264121_s-icon.jpg
new file mode 100644 (file)
index 0000000..96c18da
Binary files /dev/null and b/resources/14264121_s-icon.jpg differ
diff --git a/resources/19122178_ml-base2runner.jpg b/resources/19122178_ml-base2runner.jpg
new file mode 100644 (file)
index 0000000..6a61ed0
Binary files /dev/null and b/resources/19122178_ml-base2runner.jpg differ
diff --git a/resources/20661978_l-binary.jpg b/resources/20661978_l-binary.jpg
new file mode 100644 (file)
index 0000000..c8b0046
Binary files /dev/null and b/resources/20661978_l-binary.jpg differ
diff --git a/resources/Link to 123RF - Likeboxes.desktop b/resources/Link to 123RF - Likeboxes.desktop
new file mode 100644 (file)
index 0000000..fdcb942
--- /dev/null
@@ -0,0 +1,7 @@
+[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
diff --git a/resources/base2runner-icon.png b/resources/base2runner-icon.png
new file mode 100644 (file)
index 0000000..46f2aca
Binary files /dev/null and b/resources/base2runner-icon.png differ
diff --git a/resources/base2runner.jpg b/resources/base2runner.jpg
new file mode 100644 (file)
index 0000000..af0d868
Binary files /dev/null and b/resources/base2runner.jpg differ
diff --git a/resources/binary-1024x1024.jpg b/resources/binary-1024x1024.jpg
new file mode 100644 (file)
index 0000000..3a91c45
Binary files /dev/null and b/resources/binary-1024x1024.jpg differ
diff --git a/resources/binary-2048x2048.jpg b/resources/binary-2048x2048.jpg
new file mode 100644 (file)
index 0000000..08d3321
Binary files /dev/null and b/resources/binary-2048x2048.jpg differ