├── docs └── sphinx │ ├── .gitignore │ ├── ref-index.rst │ ├── ref │ ├── desktop.rst │ ├── modes.rst │ ├── sound.rst │ ├── events.rst │ ├── lamps.rst │ ├── config.rst │ ├── game.rst │ ├── highscore.rst │ └── dmd.rst │ ├── notes │ ├── 2011-01.rst │ ├── 2011-02.rst │ └── 2010-01.rst │ ├── index.rst │ ├── intro.rst │ ├── Makefile │ ├── tools.rst │ ├── conf.py │ └── install.rst ├── .gitignore ├── procgame ├── _version.py ├── tools │ ├── mailbox │ │ ├── __init__.py │ │ ├── clientutil.py │ │ └── mailboxclient.py │ ├── __init__.py │ ├── dmdimage.py │ ├── dmdplayer.py │ ├── cmd.py │ ├── lampshow.py │ ├── dmdfontwidths.py │ ├── config.py │ ├── dmdconvert.py │ └── dmdsplashrom.py ├── game │ └── __init__.py ├── dmd │ ├── __init__.py │ ├── animgif.py │ ├── displaycontroller.py │ ├── markup.py │ ├── dmd.py │ └── font.py ├── desktop │ ├── __init__.py │ ├── desktop_pyglet.py │ └── desktop_pygame.py ├── __init__.py ├── highscore │ ├── __init__.py │ ├── category.py │ ├── sequence.py │ └── entry.py ├── util.py ├── auxport.py ├── config.py ├── modes │ ├── __init__.py │ ├── replay.py │ ├── ballsearch.py │ ├── ballsave.py │ ├── drops.py │ └── scoredisplay.py ├── events.py ├── keyboard.py ├── sound.py └── alphanumeric.py ├── tests ├── __init__.py ├── test_events.py └── test_attrcollection.py ├── pavement.py ├── tools ├── dmdfont.py ├── dmdupdate.py ├── dmdpan.py ├── scoredisplaytest.py ├── dmdopsdemo.py ├── pygamedmdtest.py ├── dmd2mov.py └── highscoretest.py ├── setup.py └── README.markdown /docs/sphinx/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ._* 2 | *.pyc 3 | user_settings.yaml 4 | dist 5 | build 6 | pyprocgame.egg-info -------------------------------------------------------------------------------- /procgame/_version.py: -------------------------------------------------------------------------------- 1 | # Generated by: paver inc_version 2 | __version_info__ = (1, 1, 2, 1) 3 | -------------------------------------------------------------------------------- /docs/sphinx/ref-index.rst: -------------------------------------------------------------------------------- 1 | procgame Module Reference 2 | ========================= 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | :glob: 7 | 8 | ref/* 9 | -------------------------------------------------------------------------------- /procgame/tools/mailbox/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'clientutil', 3 | 'mailboxclient', 4 | ] 5 | from .clientutil import * 6 | from .mailboxclient import * 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # To run tests, from the pyprocgame folder: 2 | # 3 | # python -m unittest discover tests 'test_*.py' 4 | # 5 | from .test_attrcollection import * 6 | from .test_events import * 7 | -------------------------------------------------------------------------------- /procgame/game/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'basicgame', 3 | 'game' 4 | 'gameitems', 5 | 'mode', 6 | ] 7 | from .game import * 8 | from .gameitems import * 9 | from .basicgame import * 10 | from .mode import * 11 | -------------------------------------------------------------------------------- /docs/sphinx/ref/desktop.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | desktop Submodule 3 | ***************** 4 | 5 | .. module:: procgame.desktop 6 | 7 | Desktop 8 | ------- 9 | 10 | .. autoclass:: procgame.desktop.Desktop 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/sphinx/ref/modes.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | modes Submodule 3 | *************** 4 | 5 | .. module:: procgame.modes 6 | 7 | ScoreDisplay 8 | ------------ 9 | 10 | .. autoclass:: procgame.modes.ScoreDisplay 11 | :members: 12 | 13 | -------------------------------------------------------------------------------- /docs/sphinx/ref/sound.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | sound Submodule 3 | *************** 4 | 5 | .. module:: procgame.sound 6 | 7 | SoundController 8 | --------------- 9 | 10 | .. autoclass:: procgame.sound.SoundController 11 | :members: 12 | -------------------------------------------------------------------------------- /procgame/tools/__init__.py: -------------------------------------------------------------------------------- 1 | import yaml as _yaml 2 | import pinproc as _pinproc 3 | 4 | 5 | def machine_type_from_yaml(config_path): 6 | config = _yaml.load(open(config_path, 'r')) 7 | machine_type = config['PRGame']['machineType'] 8 | return _pinproc.normalize_machine_type(machine_type) 9 | -------------------------------------------------------------------------------- /procgame/dmd/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'animation', 3 | 'displaycontroller', 4 | 'dmd', 5 | 'font', 6 | 'layers', 7 | 'markup', 8 | 'transitions', 9 | ] 10 | from .dmd import * 11 | from .animation import * 12 | from .font import * 13 | from .layers import * 14 | from .markup import * 15 | from .transitions import * 16 | from .displaycontroller import * 17 | -------------------------------------------------------------------------------- /procgame/desktop/__init__.py: -------------------------------------------------------------------------------- 1 | # We have two implementations of the Desktop class. One for pyglet, one for pygame. 2 | # The pyglet version is prettier, so we will try to import it first. 3 | try: 4 | import pyglet as _pyglet 5 | except ImportError: 6 | _pyglet = None 7 | 8 | if _pyglet: 9 | from .desktop_pyglet import Desktop 10 | else: 11 | from .desktop_pygame import Desktop 12 | -------------------------------------------------------------------------------- /docs/sphinx/ref/events.rst: -------------------------------------------------------------------------------- 1 | **************** 2 | events Submodule 3 | **************** 4 | 5 | .. module:: procgame.events 6 | 7 | The :mod:`procgame.events` submodule contains general purpose classes for passing events around. 8 | 9 | Classes 10 | ======= 11 | 12 | Event 13 | ----- 14 | .. autoclass:: procgame.events.Event 15 | :members: 16 | 17 | EventManager 18 | ------------ 19 | .. autoclass:: procgame.events.EventManager 20 | :members: 21 | -------------------------------------------------------------------------------- /docs/sphinx/ref/lamps.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | lamps Submodule 3 | *************** 4 | 5 | .. module:: procgame.lamps 6 | 7 | Classes for controlling lamp shows. 8 | 9 | 10 | LampController 11 | -------------- 12 | 13 | .. autoclass:: procgame.lamps.LampController 14 | :members: 15 | 16 | LampShow 17 | -------- 18 | 19 | .. autoclass:: procgame.lamps.LampShow 20 | :members: 21 | 22 | LampShowMode 23 | ------------ 24 | 25 | .. autoclass:: procgame.lamps.LampShowMode 26 | :members: 27 | 28 | LampShowTrack 29 | ------------- 30 | 31 | .. autoclass:: procgame.lamps.LampShowTrack 32 | :members: 33 | 34 | Functions 35 | --------- 36 | 37 | .. autofunction:: procgame.lamps.expand_line -------------------------------------------------------------------------------- /pavement.py: -------------------------------------------------------------------------------- 1 | from paver.easy import * # for sh() 2 | 3 | 4 | @task 5 | def test(): 6 | """Run unit tests.""" 7 | import unittest 8 | import tests 9 | suite = unittest.defaultTestLoader.loadTestsFromModule(tests) 10 | unittest.TextTestRunner().run(suite) 11 | 12 | 13 | @task 14 | def revbuild(): 15 | """Increment the build number.""" 16 | import procgame 17 | version_info = procgame.__version_info__ 18 | version_info = version_info[:-1] + (int(version_info[-1]) + 1,) 19 | vfile = open('./procgame/_version.py', 'w') 20 | vfile.write('# Generated by: paver revbuild\n') 21 | vfile.write('__version_info__ = %s\n' % (repr(version_info))) 22 | vfile.close() 23 | -------------------------------------------------------------------------------- /procgame/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'config', 3 | 'dmd', 4 | 'events', 5 | 'alphanumeric', 6 | 'auxport', 7 | 'game', 8 | 'highscore', 9 | 'lamps', 10 | 'modes', 11 | 'service', 12 | 'sound', 13 | 'util', 14 | 'tools', 15 | ] 16 | 17 | from ._version import __version_info__ 18 | 19 | __version__ = '.'.join(map(str, __version_info__)) 20 | 21 | 22 | def check_version(version): 23 | """Returns true if the version of pyprocgame is greater than or equal to the supplied version tuple.""" 24 | vi = __version_info__ 25 | for n in version: 26 | if vi[0] > n: 27 | return True 28 | if vi[0] < n: 29 | return False 30 | vi = vi[1:] 31 | return True 32 | -------------------------------------------------------------------------------- /docs/sphinx/ref/config.rst: -------------------------------------------------------------------------------- 1 | **************** 2 | config Submodule 3 | **************** 4 | 5 | .. module: procgame.config 6 | 7 | The ``config`` submodule serves as a central location for pyprocgame runtime configuration settings. When this module is loaded the YAML format configuration file located at :file:`~/.pyprocgame/config.yaml` is loaded (if it exists) into :attr:`~procgame.config.values`. Other modules may then access the data structure either directly or by using the :func:`~procgame.config.value_for_key_path`. 8 | 9 | See :ref:`config-yaml` for a complete description of the system configuration file format. 10 | 11 | 12 | Members 13 | ------- 14 | 15 | .. autofunction:: procgame.config.value_for_key_path 16 | 17 | .. autodata:: procgame.config.values 18 | -------------------------------------------------------------------------------- /docs/sphinx/notes/2011-01.rst: -------------------------------------------------------------------------------- 1 | pyprocgame 1.0 Release Notes 2 | ============================ 3 | 4 | The following change notes are a summary of changes between git hash 5 | `8a769c077d1c184f5dcd `_ and `1.0 `_: 6 | 7 | pyprocgame 8 | ---------- 9 | 10 | - Added :ref:`tool-dmdsplashrom` procgame tool which enables requesting custom power-up DMD images. 11 | 12 | 13 | pypinproc 14 | --------- 15 | 16 | Added new methods supporting pinballcontroller.com's new `Power Driver Boards `_: 17 | 18 | - :meth:`pinproc.PinPROC.driver_update_global_config` 19 | - :meth:`pinproc.PinPROC.driver_update_group_config` 20 | - :meth:`pinproc.PinPROC.write_data` 21 | 22 | -------------------------------------------------------------------------------- /procgame/tools/mailbox/clientutil.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | 3 | 4 | def encode_multipart_formdata(fields, files): 5 | LIMIT = '----------lImIt_of_THE_fIle_eW_$' 6 | CRLF = '\r\n' 7 | L = [] 8 | for (key, value) in fields: 9 | L.append('--' + LIMIT) 10 | L.append('Content-Disposition: form-data; name="%s"' % key) 11 | L.append('') 12 | L.append(value) 13 | for (key, filename, value) in files: 14 | content_type = (mimetypes.guess_type(filename)[0] or 'application/octet-stream') 15 | L.append('--' + LIMIT) 16 | L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) 17 | L.append('Content-Type: %s' % content_type) 18 | L.append('') 19 | L.append(value) 20 | L.append('--' + LIMIT + '--') 21 | L.append('') 22 | body = CRLF.join(L) 23 | content_type = 'multipart/form-data; boundary=%s' % LIMIT 24 | return content_type, body 25 | -------------------------------------------------------------------------------- /docs/sphinx/index.rst: -------------------------------------------------------------------------------- 1 | .. pyprocgame documentation master file, created by 2 | sphinx-quickstart on Fri May 14 22:35:29 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | To build: 7 | make html PYTHONPATH=$PYTHONPATH:../.. 8 | 9 | pyprocgame 10 | ========== 11 | 12 | pyprocgame is a Python-based pinball software development framework for use with the P-ROC hardware. Read more about it in the :doc:`intro`, then dive into the :doc:`manual`. 13 | 14 | *This documentation is a work very much in progress. URLs subject to change.* 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | 19 | intro 20 | install 21 | manual 22 | ref-index 23 | ref-pinproc 24 | tools 25 | 26 | Release Notes 27 | ============= 28 | 29 | .. toctree:: 30 | :glob: 31 | 32 | notes/* 33 | 34 | Indices and tables 35 | ================== 36 | 37 | * :ref:`genindex` 38 | * :ref:`modindex` 39 | * :ref:`search` 40 | 41 | -------------------------------------------------------------------------------- /procgame/tools/dmdimage.py: -------------------------------------------------------------------------------- 1 | import procgame.dmd 2 | import Image 3 | import logging 4 | 5 | logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 6 | 7 | 8 | def dmd_to_image(src_filename, dst_filename): 9 | anim = procgame.dmd.Animation().load(src_filename) 10 | image = Image.new(mode='L', size=(anim.width, anim.height)) 11 | pixels = [] 12 | frame = anim.frames[0] 13 | for y in range(anim.height): 14 | for x in range(anim.width): 15 | color = frame.get_dot(x, y) 16 | pixels.append((color & 0xf) << 4) 17 | image.putdata(pixels) 18 | image.save(dst_filename) 19 | 20 | 21 | def tool_populate_options(parser): 22 | pass 23 | 24 | 25 | def tool_get_usage(): 26 | return """[options] """ 27 | 28 | 29 | def tool_run(options, args): 30 | if len(args) < 2: 31 | return False 32 | dmd_to_image(src_filename=args[0], dst_filename=args[1]) 33 | return True 34 | -------------------------------------------------------------------------------- /tools/dmdfont.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append(sys.path[ 4 | 0] + '/..') # Set the path so we can find procgame. We are assuming (stupidly?) that the first member is our directory. 5 | import pinproc 6 | from procgame import * 7 | 8 | 9 | # dmdfont.py Displays the given text on the DMD with the given text. 10 | 11 | def main(): 12 | if len(sys.argv) < 3: 13 | print("Usage: %s " % (sys.argv[0])) 14 | return 15 | 16 | font = dmd.Font(sys.argv[1]) 17 | if not font: 18 | print("Error loading font") 19 | return 20 | text = sys.argv[2] 21 | 22 | text_layer = dmd.TextLayer(0, 0, font) 23 | text_layer.set_text(text) 24 | 25 | proc = pinproc.PinPROC('wpc') 26 | w = 128 27 | h = 32 28 | proc.reset(1) 29 | 30 | grouped_layer = dmd.GroupedLayer(w, h, [dmd.FrameLayer(frame=dmd.Frame(w, h)), text_layer]) 31 | 32 | frame = grouped_layer.next_frame() 33 | if frame is None: 34 | print("No frame?") 35 | return 36 | for x in range(3): # Send it enough times to get it to show 37 | proc.dmd_draw(frame) 38 | 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /tools/dmdupdate.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append(sys.path[ 4 | 0] + '/..') # Set the path so we can find procgame. We are assuming (stupidly?) that the first member is our directory. 5 | import procgame.dmd 6 | 7 | 8 | def update(filename): 9 | """Updated the given .dmd file from legacy dot formats to the modern 0x0-0xf format.""" 10 | anim = procgame.dmd.Animation() 11 | anim.load(filename) 12 | upgrade_frames = anim.frames 13 | if len(upgrade_frames) == 2: 14 | upgrade_frames = upgrade_frames[:1] # assume it's a font; only do the first frame 15 | for frame in upgrade_frames: 16 | for x in range(frame.width): 17 | for y in range(frame.height): 18 | dot = frame.get_dot(x, y) 19 | if 0 <= dot <= 3: 20 | dot *= 5 21 | else: 22 | dot -= 0xF 23 | frame.set_dot(x, y, dot) 24 | anim.save(filename) 25 | 26 | 27 | def main(): 28 | if len(sys.argv) < 2: 29 | print("Usage: %s " % (sys.argv[0])) 30 | return 31 | update(filename=sys.argv[1]) 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /procgame/highscore/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'category', 3 | 'entry', 4 | 'sequence', 5 | ] 6 | 7 | import locale 8 | 9 | from .category import * 10 | from .entry import * 11 | from .sequence import * 12 | from .. import dmd 13 | 14 | 15 | def generate_highscore_frames(categories): 16 | """Utility function that returns a sequence of :class:`~procgame.dmd.Frame` objects 17 | describing the current high scores in each of the *categories* supplied. 18 | *categories* should be a list of :class:`HighScoreCategory` objects. 19 | """ 20 | markup = dmd.MarkupFrameGenerator() 21 | frames = list() 22 | for category in categories: 23 | for index, score in enumerate(category.scores): 24 | score_str = locale.format("%d", score.score, True) # Add commas to the score. 25 | if score.score == 1: 26 | score_str += category.score_suffix_singular 27 | else: 28 | score_str += category.score_suffix_plural 29 | text = '[%s]\n#%s#\n[%s]' % (category.titles[index], score.inits, score_str) 30 | frame = markup.frame_for_markup(markup=text, y_offset=4) 31 | frames.append(frame) 32 | return frames 33 | -------------------------------------------------------------------------------- /tools/dmdpan.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append(sys.path[ 4 | 0] + '/..') # Set the path so we can find procgame. We are assuming (stupidly?) that the first member is our directory. 5 | from procgame import * 6 | 7 | 8 | # dmdpan.py: Demonstrates how to load a large .dmd image and pan it about on the DMD. 9 | 10 | class Game(game.BasicGame): 11 | """Very simple game to get our DMD running.""" 12 | 13 | def __init__(self, machine_type): 14 | super(Game, self).__init__(machine_type) 15 | 16 | def pan(self, frame, origin, translate): 17 | mode = game.Mode(self, 9) 18 | mode.layer = dmd.PanningLayer(width=128, height=32, frame=frame, origin=origin, translate=translate) 19 | self.modes.add(mode) 20 | 21 | 22 | def main(): 23 | if len(sys.argv) < 2: 24 | print("Usage: %s " % (sys.argv[0])) 25 | return 26 | 27 | filename = sys.argv[1] 28 | anim = dmd.Animation().load(filename) 29 | if anim.width < 128 and anim.height < 32: 30 | raise ValueError("Expected animation dimensions to be 128x32 (on one side)") 31 | 32 | game = Game('wpc') 33 | 34 | game.pan(frame=anim.frames[0], origin=(0, 0), translate=(1, 1)) 35 | 36 | game.run_loop() 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /procgame/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def find_file_in_path(name, paths): 6 | """Search *paths* for a file named *name*. Return the path, or ``None`` if not found.""" 7 | for path in paths: 8 | path = os.path.join(os.path.expanduser(path), name) 9 | if os.path.isfile(path): 10 | return path 11 | return None 12 | 13 | 14 | def get_class(kls, path_adj='/.'): 15 | """Returns a class for the given fully qualified class name, *kls*. 16 | 17 | Source: http://stackoverflow.com/questions/452969/does-python-have-an-equivalent-to-java-class-forname""" 18 | sys.path.append(sys.path[0] + path_adj) 19 | parts = kls.split('.') 20 | module = ".".join(parts[:-1]) 21 | m = __import__(module) 22 | for comp in parts[1:]: 23 | m = getattr(m, comp) 24 | return m 25 | 26 | 27 | class const: 28 | """From http://code.activestate.com/recipes/65207/""" 29 | 30 | def __setattr__(self, attr, value): 31 | if hasattr(self, attr): 32 | raise ValueError('const %s already has a value and cannot be written to' % attr) 33 | self.__dict__[attr] = value 34 | 35 | 36 | class BlackHole(object): 37 | def __init__(self, *args): 38 | pass 39 | 40 | def noop(self, *args, **kwargs): 41 | pass 42 | 43 | def __getattr__(self, name): 44 | return self.noop 45 | -------------------------------------------------------------------------------- /tests/test_events.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from procgame.events import EventManager 4 | 5 | TEST_EVENT = 'test' 6 | 7 | 8 | class EventsTest(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.events = EventManager() 12 | self.events.add_event_handler(name=TEST_EVENT, object=None, handler=self.handler_no_obj) 13 | self.events.add_event_handler(name=TEST_EVENT, object=self, handler=self.handler_obj) 14 | self.flag = 0 15 | 16 | def handler_no_obj(self, event): 17 | self.flag += 1 18 | 19 | def handler_obj(self, event): 20 | self.flag += 2 21 | 22 | def test_setup(self): 23 | self.assertEqual(self.flag, 0) 24 | 25 | def test_unknown_name(self): 26 | self.events.post(name=TEST_EVENT + 'blah', object=None) 27 | self.assertEqual(self.flag, 0) 28 | 29 | def test_None_obj(self): 30 | self.events.post(name=TEST_EVENT, object=None) 31 | self.assertEqual(self.flag, 2) 32 | 33 | def test_obj_set(self): 34 | self.events.post(name=TEST_EVENT, object=self) 35 | self.assertEqual(self.flag, 3) 36 | 37 | def test_obj_other(self): 38 | self.events.post(name=TEST_EVENT, object='1234') 39 | self.assertEqual(self.flag, 1) 40 | 41 | def test_remove(self): 42 | self.events.remove_event_handler(handler=self.handler_obj) 43 | self.events.post(name=TEST_EVENT, object=self) 44 | self.assertEqual(self.flag, 1) 45 | 46 | 47 | if __name__ == '__main__': 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /docs/sphinx/ref/game.rst: -------------------------------------------------------------------------------- 1 | ************** 2 | game Submodule 3 | ************** 4 | 5 | .. module:: procgame.game 6 | 7 | The :mod:`procgame.game` submodule contains the core building blocks of a pyprocgame-based game. 8 | See :doc:`/manual` for a discussion on how to create a pyprocgame game. 9 | 10 | Core Classes 11 | ============ 12 | 13 | AttrCollection 14 | -------------- 15 | .. autoclass:: procgame.game.AttrCollection 16 | :members: 17 | 18 | Driver 19 | ------ 20 | .. autoclass:: procgame.game.Driver 21 | :members: 22 | 23 | GameController 24 | -------------- 25 | .. autoclass:: procgame.game.GameController 26 | :members: 27 | 28 | GameItem 29 | -------- 30 | .. autoclass:: procgame.game.GameItem 31 | :members: 32 | 33 | Mode 34 | ---- 35 | .. autoclass:: procgame.game.Mode 36 | :members: 37 | 38 | ModeQueue 39 | --------- 40 | .. autoclass:: procgame.game.ModeQueue 41 | :members: 42 | 43 | Player 44 | ------ 45 | .. autoclass:: procgame.game.Player 46 | :members: 47 | 48 | Switch 49 | ------ 50 | .. autoclass:: procgame.game.Switch 51 | :members: 52 | 53 | Helper Classes 54 | ============== 55 | 56 | BasicGame 57 | --------- 58 | .. autoclass:: procgame.game.BasicGame 59 | :members: 60 | 61 | 62 | Constants 63 | ========= 64 | 65 | .. data:: procgame.game.SwitchContinue 66 | 67 | Used as a return value from a :class:`~procgame.game.Mode` switch handler to indicate that lower priority modes should receive this switch event. 68 | 69 | .. data:: procgame.game.SwitchStop 70 | 71 | Used as a return value from a :class:`~procgame.game.Mode` switch handler to indicate that lower priority modes should not receive this switch event. 72 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Installs pyprocgame using distutils. 2 | 3 | Run: 4 | python setup.py install 5 | 6 | to install this package. 7 | 8 | 9 | If you want to develop using the source code directly and not install, you can run: 10 | 11 | python setup.py develop 12 | 13 | python setup.py develop --uninstall 14 | 15 | See "Development Mode" at http://peak.telecommunity.com/DevCenter/setuptool for more. 16 | 17 | """ 18 | VERSION = '2.0.0' 19 | 20 | from ez_setup import use_setuptools 21 | 22 | use_setuptools() 23 | 24 | try: 25 | from setuptools import setup 26 | except ImportError: 27 | from distutils.core import setup 28 | 29 | import sys 30 | import os 31 | 32 | setup( 33 | name='pyprocgame', 34 | version=VERSION, 35 | description='A Python-based pinball software development framework for use with P-ROC.', 36 | long_description=open('README.markdown').read(), 37 | license='MIT License', 38 | url='http://pyprocgame.pindev.org/', 39 | author='Adam Preble and Gerry Stellenberg', 40 | author_email='pyprocgame@pindev.org', 41 | packages=['procgame', 'procgame.dmd', 'procgame.game', 'procgame.highscore', 'procgame.modes', 'procgame.desktop', 42 | 'procgame.tools'], 43 | zip_safe=True, # False for non-zipped install 44 | # This works but it copies the files into /System/Library/Frameworks/Python.framework/Versions/2.6/tools -- not good 45 | # data_files = [ 46 | # ('tools', ['tools/dmdconvert.py', 'tools/dmdplayer.py']), 47 | # ], 48 | # scripts = [os.path.join('procgame', 'procgame')], 49 | entry_points={ 50 | 'console_scripts': [ 51 | 'procgame = procgame.tools.cmd:main', 52 | ] 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /procgame/auxport.py: -------------------------------------------------------------------------------- 1 | import pinproc 2 | 3 | 4 | class AuxPort(object): 5 | 6 | def __init__(self, game): 7 | super(AuxPort, self).__init__() 8 | 9 | self.game = game 10 | self.commands = [] 11 | 12 | self.init_aux_mem() 13 | 14 | def init_aux_mem(self): 15 | commands = [] 16 | commands += [pinproc.aux_command_disable()] 17 | 18 | for j in range(1, 255): 19 | commands += [pinproc.aux_command_jump(0)] 20 | 21 | self.game.proc.aux_send_commands(0, commands) 22 | 23 | def get_index(self): 24 | new_list = [] 25 | self.commands += [None] 26 | return len(self.commands) - 1 27 | 28 | def update(self, index, commands): 29 | self.commands[index] = commands 30 | self.write_commands() 31 | 32 | def write_commands(self): 33 | 34 | # Initialize command list and disable the first end so program 35 | # doesn't start 'running' until fully written. 36 | commands = [] 37 | commands += [pinproc.aux_command_disable()] 38 | 39 | for command_set in self.commands: 40 | if command_set: commands += command_set 41 | 42 | commands += [pinproc.aux_command_jump(0)] 43 | self.game.proc.aux_send_commands(0, commands) 44 | 45 | commands = [] 46 | commands += [pinproc.aux_command_jump(1)] 47 | self.game.proc.aux_send_commands(0, commands) 48 | 49 | if True: self.print_commands(commands) 50 | 51 | def print_commands(self, commands): 52 | ctr = 0 53 | print("AuxPort commands being written:") 54 | for command in commands: 55 | print("Command %d: %s" % ctr, command) 56 | ctr += 1 57 | -------------------------------------------------------------------------------- /procgame/tools/dmdplayer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pinproc 4 | 5 | import procgame.dmd 6 | import procgame.game 7 | 8 | logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 9 | 10 | 11 | class PlayerGame(procgame.game.BasicGame): 12 | anim_layer = None 13 | 14 | def __init__(self, machine_type): 15 | super(PlayerGame, self).__init__(machine_type) 16 | self.anim_layer = procgame.dmd.AnimatedLayer() 17 | mode = procgame.game.Mode(game=self, priority=1) 18 | mode.layer = self.anim_layer 19 | self.modes.add(mode) 20 | 21 | def play(self, filename, repeat): 22 | anim = procgame.dmd.Animation().load(filename) 23 | self.anim_layer.frames = anim.frames 24 | self.anim_layer.repeat = repeat 25 | if not repeat: 26 | self.anim_layer.add_frame_listener(-1, self.end_of_animation) 27 | 28 | def end_of_animation(self): 29 | self.end_run_loop() 30 | 31 | 32 | def tool_populate_options(parser): 33 | parser.add_option('-m', '--machine-type', action='store', 34 | help='wpc, wpc95, stermSAM, sternWhitestar or custom (default)') 35 | parser.add_option('-r', '--repeat', action='store_true', help='Repeat the animation indefinitely') 36 | 37 | 38 | def tool_get_usage(): 39 | return """""" 40 | 41 | 42 | def tool_run(options, args): 43 | if len(args) != 1: 44 | return False 45 | 46 | if options.machine_type: 47 | machine_type = pinproc.normalize_machine_type(options.machine_type) 48 | else: 49 | machine_type = pinproc.MachineTypeCustom 50 | 51 | game = PlayerGame(machine_type=machine_type) 52 | 53 | game.play(filename=args[0], repeat=options.repeat) 54 | 55 | game.run_loop() 56 | del game 57 | return True 58 | -------------------------------------------------------------------------------- /procgame/tools/cmd.py: -------------------------------------------------------------------------------- 1 | import optparse 2 | import os 3 | import sys 4 | 5 | import procgame 6 | 7 | commands = { 8 | 'config': 'Configuration tool.', 9 | 'dmdconvert': 'Converts image files to .dmd files.', 10 | 'dmdfontwidths': 'Interactively assign font width values.', 11 | 'dmdimage': 'Converts .dmd files to image files.', 12 | 'dmdsplashrom': 'Requests a new P-ROC image with a custom DMD splash image.', 13 | 'dmdplayer': 'Play a .dmd file.', 14 | 'lampshow': 'Play a lamp show.', 15 | } 16 | 17 | 18 | def main(): 19 | """Command line main for 'procgame'. 20 | 21 | To create a command, it must reside in this module and have the following 22 | methods: 23 | 24 | tool_get_usage() 25 | tool_populate_options(parser) 26 | tool_run(options, args) 27 | 28 | """ 29 | 30 | show_help = False 31 | 32 | if len(sys.argv) <= 1: 33 | show_help = True 34 | elif not sys.argv[1] in commands.keys(): 35 | show_help = True 36 | 37 | if show_help: 38 | print("""Usage: %s ... """ % (os.path.basename(sys.argv[0]))) 39 | print("") 40 | print("Commands:") 41 | for name in sorted(commands.keys()): 42 | print(" % -16s %s" % name, commands[name]) 43 | sys.exit(1) 44 | 45 | command_name = sys.argv[1] 46 | __import__('procgame.tools.' + command_name) 47 | module = getattr(procgame.tools, command_name) 48 | 49 | parser = optparse.OptionParser() 50 | parser.usage = """%s %s %s""" % (os.path.basename(sys.argv[0]), sys.argv[1], module.tool_get_usage()) 51 | module.tool_populate_options(parser) 52 | 53 | (options, args) = parser.parse_args(sys.argv[2:]) 54 | 55 | ok = module.tool_run(options, args) 56 | if not ok: 57 | parser.print_help() 58 | sys.exit(1) 59 | -------------------------------------------------------------------------------- /procgame/tools/lampshow.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | import procgame.game 6 | import procgame.lamps 7 | import procgame.tools 8 | 9 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 10 | 11 | 12 | class LampGame(procgame.game.GameController): 13 | show_filename = None 14 | show_mtime = None 15 | 16 | def __init__(self, machine_type): 17 | super(LampGame, self).__init__(machine_type) 18 | self.lampctrl = procgame.lamps.LampController(game=self) 19 | 20 | def play(self, filename): 21 | self.show_filename = filename 22 | self.show_mtime = None 23 | 24 | def tick(self): 25 | super(LampGame, self).tick() 26 | mtime = os.path.getmtime(self.show_filename) 27 | if self.show_mtime != mtime: 28 | logging.getLogger('').info('Loading lamp show at %s.', self.show_filename) 29 | self.lampctrl.register_show('show', self.show_filename) 30 | self.lampctrl.play_show('show', repeat=True) 31 | self.show_mtime = mtime 32 | 33 | 34 | def play(config_path, show_path): 35 | game = LampGame(machine_type=procgame.tools.machine_type_from_yaml(config_path)) 36 | game.load_config(config_path) 37 | game.play(show_path) 38 | game.run_loop() 39 | del game 40 | 41 | 42 | def tool_populate_options(parser): 43 | parser.add_option('-c', '--config', action='store', help='Path to the YAML machine configuration file.') 44 | 45 | 46 | def tool_get_usage(): 47 | return """""" 48 | 49 | 50 | def tool_run(options, args): 51 | if len(args) != 1: 52 | return False 53 | 54 | if not options.config: 55 | sys.stderr.write('No configuration file specified.\n') 56 | return False 57 | 58 | return play(config_path=options.config, show_path=args[0]) 59 | -------------------------------------------------------------------------------- /tests/test_attrcollection.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from procgame.game import AttrCollection, GameItem 4 | 5 | TEST_EVENT = 'test' 6 | 7 | 8 | class EventsTest(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.attrs = AttrCollection() 12 | item = GameItem(game=None, name='a', number=1) 13 | item.tags.append('awesome') 14 | self.attrs.add(item.name, item) 15 | item = GameItem(game=None, name='b', number=2) 16 | self.attrs.add(item.name, item) 17 | 18 | def test_len(self): 19 | self.assertEqual(len(self.attrs), 2) 20 | 21 | def test_iter(self): 22 | count = 0 23 | for item in self.attrs: 24 | count += 1 25 | self.assertEqual(count, 2) 26 | 27 | def test_tags(self): 28 | items = self.attrs.items_tagged('awesome') 29 | self.assertEqual(len(items), 1) 30 | items = self.attrs.items_tagged('does-not-exist') 31 | self.assertEqual(len(items), 0) 32 | 33 | def test_attrlookup_str(self): 34 | item = self.attrs['a'] 35 | self.assertEqual(item.name, 'a') 36 | self.assertEqual(item.number, 1) 37 | 38 | def test_attrlookup_num(self): 39 | item = self.attrs[1] 40 | self.assertEqual(item.name, 'a') 41 | self.assertEqual(item.number, 1) 42 | 43 | def test_remove(self): 44 | self.attrs.remove(name='a', number=1) 45 | self.assertEqual(len(self.attrs), 1) 46 | 47 | def test_membership(self): 48 | self.assertTrue(1 in self.attrs) 49 | self.assertTrue(2 in self.attrs) 50 | self.assertTrue('a' in self.attrs) 51 | self.assertTrue('b' in self.attrs) 52 | self.assertFalse('x' in self.attrs) 53 | 54 | def test_filter(self): 55 | items = filter(None, self.attrs) 56 | self.assertEqual(len(items), 2) 57 | items = filter(lambda item: item.name is not None, self.attrs) 58 | self.assertEqual(len(items), 2) 59 | 60 | 61 | if __name__ == '__main__': 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /tools/scoredisplaytest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append(sys.path[ 4 | 0] + '/..') # Set the path so we can find procgame. We are assuming (stupidly?) that the first member is our directory. 5 | import pinproc 6 | from procgame import * 7 | import random 8 | import locale 9 | 10 | locale.setlocale(locale.LC_ALL, "") # Used to put commas in the score. 11 | 12 | config_path = "JD.yaml" 13 | 14 | 15 | class ScoreTester(game.Mode): 16 | left_players_justify_left = True 17 | 18 | def sw_flipperLwL_active(self, sw): 19 | self.game.score(random.randint(0, 100000) * 10) 20 | return True 21 | 22 | def sw_flipperLwR_active(self, sw): 23 | self.game.end_ball() 24 | return True 25 | 26 | def sw_fireL_active(self, sw): 27 | self.left_players_justify_left = not self.left_players_justify_left 28 | if self.left_players_justify_left: 29 | self.game.score_display.set_left_players_justify("left") 30 | else: 31 | self.game.score_display.set_left_players_justify("right") 32 | return True 33 | 34 | 35 | class TestGame(game.BasicGame): 36 | """docstring for TestGame""" 37 | 38 | def setup(self): 39 | """docstring for setup""" 40 | self.load_config(config_path) 41 | self.reset() 42 | 43 | self.start_game() 44 | 45 | for i in range(4): 46 | self.add_player() 47 | self.players[i].score = random.randint(0, 1e5) * 10 48 | 49 | self.current_player_index = 0 # random.randint(0, 3) 50 | 51 | self.start_ball() 52 | 53 | def reset(self): 54 | super(TestGame, self).reset() 55 | self.modes.add(ScoreTester(self, 5)) 56 | # Make sure flippers are off, especially for user-initiated resets. 57 | self.enable_flippers(enable=False) 58 | 59 | 60 | def main(): 61 | game = None 62 | try: 63 | game = TestGame(pinproc.MachineTypeWPC) 64 | game.setup() 65 | game.run_loop() 66 | finally: 67 | del game 68 | 69 | 70 | if __name__ == '__main__': main() 71 | -------------------------------------------------------------------------------- /procgame/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import yaml 5 | 6 | values = None 7 | """The configuration data structure loaded from :file:`~/.pyprocgame/config.yaml` when this submodule is loaded.""" 8 | 9 | path = None 10 | """Path that the configuration data structure was loaded from, by :meth:`load`.""" 11 | 12 | 13 | def value_for_key_path(keypath, default=None): 14 | """Returns the value at the given *keypath* within :attr:`values`. 15 | 16 | A key path is a list of components delimited by dots (periods). The components are interpreted 17 | as dictionary keys within the structure. 18 | For example, the key path ``'a.b'`` would yield ``'c'`` with the following :attr:`values` dictionary: :: 19 | 20 | {'a':{'b':'c'}} 21 | 22 | If the key path does not exist *default* will be returned. 23 | """ 24 | v = values 25 | for component in keypath.split('.'): 26 | if v is not None and v.has_key(component): 27 | v = v[component] 28 | else: 29 | v = default 30 | return v 31 | 32 | 33 | def load(): 34 | global values, path 35 | logger = logging.getLogger('game.config') 36 | curr_path = os.path.expanduser('./config.yaml') 37 | system_path = os.path.expanduser('~/.pyprocgame/config.yaml') 38 | if os.path.exists(curr_path): 39 | path = curr_path 40 | else: 41 | logger.warning('pyprocgame configuration not found at %s. Checking %s.' % (curr_path, system_path)) 42 | if os.path.exists(system_path): 43 | path = system_path 44 | else: 45 | logger.warning('pyprocgame configuration not found at %s' % system_path) 46 | return 47 | logger.info('pyprocgame configuration found at %s' % path) 48 | try: 49 | values = yaml.load(open(path, 'r')) 50 | except yaml.scanner.ScannerError as e: 51 | logger.error( 52 | 'Error loading pyprocgame config from %s; your configuration file has a syntax error in it!\nDetails: %s', 53 | path, e) 54 | except Exception as e: 55 | logger.error('Error loading pyprocgame config from %s: %s', path, e) 56 | 57 | 58 | load() 59 | -------------------------------------------------------------------------------- /tools/dmdopsdemo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append(sys.path[ 4 | 0] + '/..') # Set the path so we can find procgame. We are assuming (stupidly?) that the first member is our directory. 5 | from procgame import * 6 | import time 7 | 8 | 9 | # dmdopsdemo.py demonstrates how to use Layer.composite_op. 10 | 11 | class Game(game.BasicGame): 12 | """Very simple game to get our DMD running.""" 13 | 14 | def __init__(self, machine_type): 15 | super(Game, self).__init__(machine_type) 16 | self.dmd = dmd.DisplayController(self, width=128, height=32) 17 | self.frame_count = 0 18 | 19 | def dmd_event(self): 20 | """Called by the GameController when a DMD event has been received.""" 21 | self.dmd.update() 22 | if self.frame_count == 0: 23 | self.first_frame_time = time.time() 24 | self.frame_count += 1 25 | if self.frame_count == 200: 26 | secs = time.time() - self.first_frame_time 27 | print("%d frames, %0.2f seconds, %0.2ffps" % (self.frame_count, secs, self.frame_count / secs)) 28 | 29 | def play(self, anim): 30 | font = dmd.font_named('Font18x12.dmd') 31 | mode = game.Mode(self, 9) 32 | anim_layer = dmd.AnimatedLayer(frames=anim.frames, repeat=True, hold=False) 33 | text_layer = dmd.TextLayer(128 / 2, 8, font, 'center').set_text('EXTRA BALL') 34 | text_layer.composite_op = 'sub' 35 | mode.layer = dmd.GroupedLayer(width=128, height=32) 36 | mode.layer.layers += [anim_layer] 37 | mode.layer.layers += [text_layer] 38 | self.modes.add(mode) 39 | 40 | 41 | def main(): 42 | if len(sys.argv) < 2: 43 | print("Usage: %s " % (sys.argv[0])) 44 | return 45 | 46 | filename = sys.argv[1] 47 | anim = dmd.Animation().load(filename) 48 | if anim.width != 128 or anim.height != 32: 49 | raise ValueError("Expected animation dimensions to be 128x32.") 50 | 51 | game = Game('custom') 52 | game.play(anim=anim) 53 | 54 | print("Displaying %d frame(s) looped." % (len(anim.frames))) 55 | game.run_loop() 56 | 57 | 58 | if __name__ == "__main__": 59 | main() 60 | -------------------------------------------------------------------------------- /docs/sphinx/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | About pyprocgame 5 | ---------------- 6 | 7 | pyprocgame is a Python-based pinball software development framework for use with the P-ROC hardware. pyprocgame is written by `Adam Preble `_ and Gerry Stellenberg. 8 | 9 | Availability 10 | ------------ 11 | 12 | pyprocgame source code is available on GitHub at http://github.com/preble/pyprocgame. Documentation is available at http://pyprocgame.pindev.org/. 13 | 14 | About P-ROC 15 | ----------- 16 | 17 | P-ROC (Pinball Remote Operations Controller) is a circuit board that enables a Mac, Linux, or Windows computer with a USB port to control a full scale pinball machine. Learn more about P-ROC at http://pinballcontrollers.com. 18 | 19 | Support and Contact 20 | ------------------- 21 | 22 | Visit the `pyprocgame forum at pinballcontrollers.com `_. The authors can be contacted at pyprocgame at pindev.org. 23 | 24 | License 25 | ------- 26 | 27 | pyprocgame is made available under the `MIT License `_: 28 | 29 | Copyright (c) 2009-2011 Adam Preble and Gerry Stellenberg 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in 39 | all copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 47 | THE SOFTWARE. 48 | 49 | -------------------------------------------------------------------------------- /procgame/dmd/animgif.py: -------------------------------------------------------------------------------- 1 | import Image 2 | import procgame.dmd 3 | 4 | 5 | class ImageSequence: 6 | """Iterates over all images in a sequence (of PIL Images).""" 7 | 8 | # Source: http://www.pythonware.com/library/pil/handbook/introduction.htm 9 | def __init__(self, im): 10 | self.im = im 11 | 12 | def __getitem__(self, ix): 13 | try: 14 | if ix: 15 | self.im.seek(ix) 16 | return self.im 17 | except EOFError: 18 | raise IndexError # end of sequence 19 | 20 | 21 | def gif_frames(src): 22 | """Returns an array of frames to be added to the animation.""" 23 | frames = [] 24 | 25 | # We have to do some special stuff for animated GIFs: check for the background index, and if we get it use the last frame's value. 26 | transparent_idx = -1 27 | background_idx = -1 28 | if 'transparency' in src.info: 29 | transparent_idx = src.info['transparency'] 30 | if 'background' in src.info: 31 | background_idx = src.info['background'] 32 | last_frame = None 33 | 34 | (w, h) = src.size 35 | 36 | # Construct a lookup table from 0-255 to 0-15: 37 | eight_to_four_map = [0] * 256 38 | for l in range(256): 39 | eight_to_four_map[l] = 0xf0 + int(round((l / 255.0) * 15.0)) 40 | 41 | for src_im in ImageSequence(src): 42 | reduced = src.convert("L") 43 | 44 | frame = procgame.dmd.Frame(w, h) 45 | 46 | for x in range(w): 47 | for y in range(h): 48 | idx = src_im.getpixel((x, y)) # Get the palette index for this pixel 49 | if idx == background_idx: 50 | # background index means use the prior frame's dot data 51 | if last_frame: 52 | color = last_frame.get_dot(x, y) 53 | else: 54 | # No prior frame to refer to. 55 | color = 0xff # Don't have a good option here. 56 | elif idx == transparent_idx: 57 | color = 0x00 58 | else: 59 | color = eight_to_four_map[reduced.getpixel((x, y))] 60 | frame.set_dot(x=x, y=y, value=color) 61 | 62 | frames.append(frame) 63 | last_frame = frame 64 | 65 | return frames 66 | -------------------------------------------------------------------------------- /procgame/modes/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'ballsave', 3 | 'ballsearch', 4 | 'drops' 5 | 'replay', 6 | 'scoredisplay', 7 | 'trough', 8 | ] 9 | from .ballsave import * 10 | from .ballsearch import * 11 | from .drops import * 12 | from .replay import * 13 | from .scoredisplay import * 14 | from .trough import * 15 | 16 | from ..game import Mode 17 | 18 | 19 | class TransitionOutHelperMode(Mode): 20 | def __init__(self, game, priority, transition, layer): 21 | super(TransitionOutHelperMode, self).__init__(game=game, priority=priority) 22 | self.layer = layer 23 | self.layer.transition = transition 24 | self.layer.transition.in_out = 'out' 25 | self.layer.transition.completed_handler = self.transition_completed 26 | 27 | def mode_started(self): 28 | self.layer.transition.start() 29 | 30 | def transition_completed(self): 31 | self.game.modes.remove(self) 32 | 33 | 34 | class SwitchSequenceRecognizer(Mode): 35 | """Listens to switch events to detect and act upon sequences.""" 36 | 37 | switches = {} 38 | 39 | switch_log = [] 40 | 41 | def __init__(self, game, priority): 42 | super(SwitchSequenceRecognizer, self).__init__(game=game, priority=priority) 43 | self.switches = {} 44 | self.switch_log = [] 45 | 46 | def add_sequence(self, sequence, handler): 47 | unique_switch_names = list(set(map(lambda sw: sw.name, sequence))) 48 | sequence_switch_nums = map(lambda sw: sw.number, sequence) 49 | # sequence_str = self.switch_separator_char.join(sequence_switch_nums) 50 | self.switches[tuple(sequence_switch_nums)] = handler 51 | for sw in unique_switch_names: 52 | # No concern about duplicate switch handlers, as add_switch_handler() protects against this. 53 | self.add_switch_handler(name=sw, event_type='active', delay=None, handler=self.switch_active) 54 | 55 | def reset(self): 56 | """Resets the remembered sequence.""" 57 | self.switch_log = [] 58 | 59 | def switch_active(self, sw): 60 | self.switch_log.append(sw.number) 61 | log_tuple = tuple(self.switch_log) 62 | for sequence, handler in self.switches.items(): 63 | if log_tuple[-len(sequence):] == sequence: 64 | handler() 65 | -------------------------------------------------------------------------------- /docs/sphinx/notes/2011-02.rst: -------------------------------------------------------------------------------- 1 | pyprocgame 1.x (Development) Release Notes 2 | ========================================== 3 | 4 | The following change notes are a summary of changes since `1.0 `_: 5 | 6 | pyprocgame 7 | ---------- 8 | 9 | - Changed named parameter for :meth:`procgame.game.Driver.patter`: ``orig_on_time`` is now ``original_on_time``, to be consistent with pypinproc and other existing methods. 10 | - Added child modes. See :meth:`procgame.game.Mode.add_child_mode` for more details. 11 | - :meth:`procgame.game.Mode.delay` now has more optional parameters, and will autogenerate a unique name and return it. 12 | - Added :meth:`procgame.dmd.Font.draw_in_rect`. 13 | - Added :attr:`procgame.dmd.TextLayer.fill_color`. 14 | - :attr:`procgame.game.Switch.hw_timestamp` is now set when available. 15 | - Added :attr:`procgame.modes.ScoreDisplay.credit_string_callback`. 16 | - :meth:`procgame.dmd.DisplayController.update` now returns the frame it generated. 17 | - Added ``dmdimage`` tool. 18 | - Game configuration file: game items (switches, coils, and lamps) can now have a ``tags`` key. This allows obtaining game items with a certain tag via :meth:`procgame.game.AttrCollection.items_tagged`. 19 | - Fixed an issue where :meth:`procgame.game.Switch.time_since_change` did not behave as documented. Previously, querying :meth:`time_since_changed` would always return 0 if called within that switch's handler. Now the time will be properly reset *after* the switch's events are handled. 20 | - Added :meth:`procgame.game.GameController.load_config_stream`. 21 | - Added :class:`procgame.events.EventManager`, a general purpose event dispatcher. 22 | - In :meth:`procgame.game.GameController.end_game`, :attr:`~procgame.game.GameController.ball` is now set to 0 before calling :meth:`~procgame.game.GameController.game_ended`. 23 | - Added :meth:`procgame.game.GameController.is_game_over`. 24 | - Added 'now' parameter to :meth:`procgame.game.Driver.patter`. 25 | - Added :meth:`procgame.game.Driver.future_pulse` 26 | - Added :class:`procgame.game.PDBConfig` to handle 'pdb' machineType YAML parsing 27 | - Added 'polarity' field to YAML driver parsing 28 | - Added 'pulseTime' field to YAML driver parsing to set a driver's default pulse width. 29 | - Added 'label' field to YAML item parsing for human readable item descriptions. 30 | -------------------------------------------------------------------------------- /tools/pygamedmdtest.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | 4 | import pinproc 5 | 6 | machine_type = 'wpc' 7 | 8 | 9 | def pulse(n, t=20): 10 | """docstring for pulse""" 11 | pr.driver_pulse(pinproc.decode(str(n)), t) 12 | 13 | 14 | def main(): 15 | pr = pinproc.PinPROC(machine_type) 16 | time.sleep(2) # Give P-ROC a second to get going? 17 | import pygame 18 | try: 19 | pygame.init() 20 | screen = pygame.display.set_mode((128, 32)) 21 | pygame.display.set_caption('P-ROC DMD') 22 | 23 | background = pygame.Surface(screen.get_size()) 24 | background = background.convert() 25 | 26 | fontSize = 14 27 | x = 200.0 28 | while 1: 29 | background.fill((0, 0, 0)) 30 | 31 | if pygame.font: 32 | font = pygame.font.Font(None, fontSize * 3) 33 | text = font.render("This is P-ROC", 1, (150, 150, 150)) 34 | textpos = text.get_rect(center=(x, background.get_height() / 2)) # (centerx=background.get_width()/2) 35 | background.blit(text, textpos) 36 | font = pygame.font.Font(None, fontSize) 37 | text = font.render("This is P-ROC", 1, (255, 255, 255)) 38 | textpos = text.get_rect(center=(math.cos(time.clock() * 5.0) * 10.0 + background.get_width() / 2, 39 | math.sin(time.clock() * 5.0) * 5.0 + background.get_height() * 0.3)) 40 | background.blit(text, textpos) 41 | font = pygame.font.Font(None, fontSize * 1.5) 42 | text = font.render("pypinproc", 1, (255, 255, 255)) 43 | textpos = text.get_rect(center=(background.get_width() * 0.5, background.get_height() * 0.75)) 44 | background.blit(text, textpos) 45 | 46 | screen.blit(background, (0, 0)) 47 | pygame.display.flip() 48 | 49 | surface = pygame.display.get_surface() 50 | buffer = surface.get_buffer() 51 | 52 | pr.dmd_draw(buffer.raw) 53 | del buffer 54 | del surface 55 | x -= 1 56 | if x < -200.0: 57 | x = 200.0 58 | time.sleep(1 / 40) 59 | finally: # except KeyboardInterrupt: 60 | del pr 61 | 62 | 63 | # this calls the 'main' function when this script is executed 64 | if __name__ == '__main__': main() 65 | -------------------------------------------------------------------------------- /tools/dmd2mov.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | try: 4 | import objc 5 | from Foundation import * 6 | from QTKit import * 7 | except ImportError: 8 | print("Error importing Mac OS X PyObjC frameworks. This application requires Mac OS X.") 9 | sys.exit(1) 10 | 11 | sys.path.append(sys.path[ 12 | 0] + '/..') # Set the path so we can find procgame. We are assuming (stupidly?) that the first member is our directory. 13 | from procgame import * 14 | 15 | 16 | def NSOvalFill(rect): 17 | thePath = NSBezierPath.bezierPath() 18 | thePath.appendBezierPathWithOvalInRect_(rect) 19 | thePath.fill() 20 | 21 | 22 | colors = [] 23 | for c in range(16): 24 | q = (float(c) / 15.0) 25 | colors.append(NSColor.colorWithDeviceRed_green_blue_alpha_(1.0, 0.5, 0.0, q)) 26 | 27 | 28 | def dmd_frame_as_nsimage(frame): 29 | global colors 30 | dot_size = 8 31 | image = NSImage.alloc().initWithSize_(NSMakeSize(frame.width * dot_size, frame.height * dot_size)) 32 | image.lockFocus() 33 | NSColor.blackColor().set() 34 | NSRectFill(NSMakeRect(0, 0, frame.width * dot_size, frame.height * dot_size)) 35 | for y in range(frame.height): 36 | for x in range(frame.width): 37 | color = frame.get_dot(x, y) 38 | if color == 0: continue 39 | if not color in range(len(colors)): 40 | print("Skipping bad color", color, "at", x, y) 41 | else: 42 | colors[color].set() 43 | NSRectFill(NSMakeRect(x * dot_size + 1, (frame.height - 1 - y) * dot_size + 1, dot_size - 2, dot_size - 2)) 44 | image.unlockFocus() 45 | return image 46 | 47 | 48 | def dmd2mov(input_path, output_path, fps): 49 | movie = QTMovie.alloc().initToWritableData_error_(NSMutableData.data(), None)[0] 50 | anim = dmd.Animation().load(input_path) 51 | attrs = {QTAddImageCodecType: "mp4v"} 52 | for frame in anim.frames: 53 | image = dmd_frame_as_nsimage(frame) 54 | movie.addImage_forDuration_withAttributes_(image, QTMakeTimeWithTimeInterval(1.0 / float(fps)), attrs) 55 | movie.setCurrentTime_(movie.duration()) 56 | sys.stdout.flush() 57 | movie.writeToFile_withAttributes_(output_path, {QTMovieFlatten: True}) 58 | 59 | 60 | def main(): 61 | if len(sys.argv) < 4: 62 | print("Usage: %s " % (sys.argv[0])) 63 | return 64 | dmd2mov(sys.argv[1], sys.argv[2], fps=int(sys.argv[3])) 65 | 66 | 67 | if __name__ == "__main__": 68 | main() 69 | -------------------------------------------------------------------------------- /procgame/events.py: -------------------------------------------------------------------------------- 1 | class Event(object): 2 | """Describes an event dispatched by :class:`EventManager`.""" 3 | 4 | name = None 5 | """Name of the event.""" 6 | object = None 7 | """Object this event is associated with.""" 8 | info = None 9 | """Any information associated with the event.""" 10 | 11 | def __init__(self, name, object, info): 12 | super(Event, self).__init__() 13 | self.name = name 14 | self.object = object 15 | self.info = info 16 | 17 | 18 | global_event_manager = None 19 | 20 | 21 | class EventManager(object): 22 | """Dispatches events to event handlers. Until better documentation is created, it may be helpful to know that this class is strongly influenced by the Cocoa class :class:`NSNotificationCenter`. 23 | 24 | Most users will want to obtain the default instance using :meth:`default`:: 25 | 26 | EventManager.default().add_event_handler(...) 27 | """ 28 | 29 | @classmethod 30 | def default(cls): 31 | """Returns the default (shared) EventManager instance.""" 32 | global global_event_manager 33 | if not global_event_manager: 34 | global_event_manager = cls() 35 | return global_event_manager 36 | 37 | def __init__(self): 38 | super(EventManager, self).__init__() 39 | # __handlers is keyed off of the event name, with the contents being a hash of the object to the handler arrays: 40 | # __handlers[name][object][handler_index] 41 | self.__handlers = {} 42 | 43 | def add_event_handler(self, name, handler, object=None): 44 | """Handlers take a single parameter, the :class:`Event` object being posted.""" 45 | if name not in self.__handlers: 46 | self.__handlers[name] = {object: [handler]} 47 | else: 48 | obj_keyed = self.__handlers[name] 49 | if object not in obj_keyed: 50 | obj_keyed[object] = [handler] 51 | elif handler not in obj_keyed[object]: 52 | obj_keyed[object].append(handler) 53 | 54 | def remove_event_handler(self, handler): 55 | """Remove the given handler.""" 56 | # Search __handlers to remove it. 57 | for name in self.__handlers: 58 | obj_keyed = self.__handlers[name] 59 | for object in obj_keyed: 60 | handlers = obj_keyed[object] 61 | if handler in handlers: 62 | handlers.remove(handler) 63 | 64 | def post_event(self, event): 65 | """Post the given :class:`Event` instance. Blocks while the resulting handlers are called.""" 66 | if event.name in self.__handlers: 67 | obj_keyed = self.__handlers[event.name] 68 | if None in obj_keyed: 69 | for handler in obj_keyed[None]: 70 | handler(event) 71 | if event.object in obj_keyed: 72 | for handler in obj_keyed[event.object]: 73 | handler(event) 74 | 75 | def post(self, name, object=None, info=None): 76 | """Post an :class:`Event` with the given properties.""" 77 | event = Event(name, object, info) 78 | self.post_event(event) 79 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## pyprocgame 2 | 3 | pyprocgame is a high-level pinball development framework for use with P-ROC (Pinball Remote Operations Controller). It was written by Adam Preble and Gerry Stellenberg. More information about P-ROC is available at [pinballcontrollers.com](http://pinballcontrollers.com/). See the [pyprocgame site](http://pyprocgame.pindev.org/) for the full pyprocgame documentation. 4 | 5 | ## Prerequisites 6 | 7 | pyprocgame requires the following: 8 | 9 | - [Python 3.7+](http://python.org/) 10 | - [pypinproc](http://github.com/preble/pypinproc) -- native Python extension enabling P-ROC hardware access and native DMD frame manipulation. 11 | - [setuptools](http://pypi.python.org/pypi/setuptools) -- required for procgame script installation. Also adds easy\_install, a helpful installer tool for popular Python modules. 12 | - [pyyaml](http://pyyaml.org/) -- YAML parsing. 13 | - One of the Python graphics and sound modules: 14 | - [pyglet](http://www.pyglet.org/) 15 | - [pygame](http://www.pygame.org/) 16 | - [Python Imaging Library](http://www.pythonware.com/products/pil/) (PIL) 17 | 18 | ## Installation 19 | 20 | To install pyprocgame: (depending on your system configuration you may need to use _sudo_) 21 | 22 | python setup.py install 23 | 24 | This will install pyprocgame such that you can import it from any Python script on your system: 25 | 26 | import procgame.game 27 | 28 | It will also install the "procgame" command line tool into a system-dependent location. On Linux and Mac OS X systems this will probably be in your path such that you can type, from the command line: 29 | 30 | procgame 31 | 32 | and see a list of available commands. If it is not in your path you can invoke it directly, or modify your PATH environment variable. Note that on Windows the procgame script is typically located in C:\Python26\Scripts. 33 | 34 | ## Documentation 35 | 36 | Please see the [pyprocgame Documentation](http://pyprocgame.pindev.org/) site for the pyprocgame Manual and detailed API documentation. 37 | 38 | ## License 39 | 40 | Copyright (c) 2009-2020 Adam Preble, Gerry Stellenberg, Jimmy Lipham, Michael Ocean & Other Contributors 41 | 42 | Permission is hereby granted, free of charge, to any person obtaining a copy 43 | of this software and associated documentation files (the "Software"), to deal 44 | in the Software without restriction, including without limitation the rights 45 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 46 | copies of the Software, and to permit persons to whom the Software is 47 | furnished to do so, subject to the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be included in 50 | all copies or substantial portions of the Software. 51 | 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 53 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 54 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 55 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 56 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 57 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 58 | THE SOFTWARE. -------------------------------------------------------------------------------- /procgame/dmd/displaycontroller.py: -------------------------------------------------------------------------------- 1 | from .dmd import * 2 | from .layers import * 3 | 4 | 5 | class DisplayController(object): 6 | """Manages the process of obtaining DMD frames from active modes and compositing them together for 7 | display on the DMD. 8 | 9 | **Using DisplayController** 10 | 11 | 1. Add a :class:`DisplayController` instance to your :class:`~procgame.game.GameController` subclass:: 12 | 13 | class Game(game.GameController): 14 | def __init__(self, machine_type): 15 | super(Game, self).__init__(machine_type) 16 | self.dmd = dmd.DisplayController(self, width=128, height=32, 17 | message_font=font_tiny7) 18 | 19 | 2. In your subclass's :meth:`~procgame.game.GameController.dmd_event` call :meth:`DisplayController.update`:: 20 | 21 | def dmd_event(self): 22 | self.dmd.update() 23 | 24 | """ 25 | 26 | frame_handlers = [] 27 | """If set, frames obtained by :meth:`.update` will be sent to the functions 28 | in this list with the frame as the only parameter. 29 | 30 | This list is initialized to contain only ``self.game.proc.dmd_draw``.""" 31 | 32 | def __init__(self, game, width=128, height=32, message_font=None): 33 | self.game = game 34 | self.message_layer = None 35 | self.width = width 36 | self.height = height 37 | if message_font is not None: 38 | self.message_layer = TextLayer(width / 2, height - 2 * 7, message_font, "center") 39 | # Do two updates to get the pump primed: 40 | for x in range(2): 41 | self.update() 42 | self.frame_handlers.append(self.game.proc.dmd_draw) 43 | 44 | def set_message(self, message, seconds): 45 | if self.message_layer is None: 46 | raise ValueError("Message_font must be specified in constructor to enable message layer.") 47 | self.message_layer.set_text(message, seconds) 48 | 49 | def update(self): 50 | """Iterates over :attr:`procgame.game.GameController.modes` from lowest to highest 51 | and composites a DMD image for this 52 | point in time by checking for a ``layer`` attribute on each :class:`~procgame.game.Mode`. 53 | If the mode has a layer attribute, that layer's :meth:`~procgame.dmd.Layer.composite_next` method is called 54 | to apply that layer's next frame to the frame in progress. 55 | 56 | The resulting frame is sent to the :attr:`frame_handlers` and then returned from this method.""" 57 | layers = [] 58 | for mode in self.game.modes.modes: 59 | if hasattr(mode, 'layer') and mode.layer is not None: 60 | layers.append(mode.layer) 61 | if mode.layer.opaque: 62 | break # if we have an opaque layer we don't render any lower layers 63 | 64 | frame = Frame(self.width, self.height) 65 | for layer in layers[::-1]: # We reverse the list here so that the top layer gets the last say. 66 | if layer.enabled: 67 | layer.composite_next(frame) 68 | 69 | if self.message_layer is not None: 70 | self.message_layer.composite_next(frame) 71 | 72 | if frame is not None: 73 | for handler in self.frame_handlers: 74 | handler(frame) 75 | 76 | return frame 77 | -------------------------------------------------------------------------------- /docs/sphinx/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyprocgame.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyprocgame.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | -------------------------------------------------------------------------------- /procgame/tools/dmdfontwidths.py: -------------------------------------------------------------------------------- 1 | # dmdfondwidths.py 2 | # Helper tool for setting character widths on P-ROC .dmd-based fonts. 3 | # 4 | # Opens the given .dmd font file and displays the given text on the DMD with that font. 5 | # Type in a character (or set of characters), hit return, then type the width of that character (or set of characters). 6 | # The DMD will be updated to display the text with the new character width properties. Hit enter at the character 7 | # prompt to save to the .dmd file and exit the program. 8 | # 9 | import sys 10 | 11 | import pinproc 12 | 13 | from procgame import dmd 14 | from procgame import game 15 | 16 | 17 | class DmdFontWidthsGame(game.BasicGame): 18 | def __init__(self, font, font_path, text): 19 | super(DmdFontWidthsGame, self).__init__(pinproc.MachineTypeCustom) 20 | self.reset() 21 | w = 128 22 | h = 32 23 | self.font = font 24 | self.font_path = font_path 25 | self.text = text 26 | self.text_layer = dmd.TextLayer(0, 0, font) 27 | self.text_layer.set_text(text) 28 | mode = game.Mode(game=self, priority=9) 29 | mode.layer = dmd.GroupedLayer(w, h, [dmd.FrameLayer(frame=dmd.Frame(w, h)), self.text_layer]) 30 | self.modes.add(mode) 31 | self.dirty = False 32 | 33 | def show_last_frame(self): 34 | # Override BasicGame's show_last_frame() in order to pause and wait for user input. 35 | # This means that the game loop only runs after each user input. 36 | if self.desktop and self.last_frame: 37 | self.desktop.draw(self.last_frame) 38 | self.last_frame = None 39 | print("Enter character(s) to set width of: ") 40 | try: 41 | chars = sys.stdin.readline() 42 | if len(chars) == 1: 43 | print("Got empty line; exiting.") 44 | if self.dirty: 45 | print("Saving font to %s" % self.font_path) 46 | self.font.save(self.font_path) 47 | else: 48 | print("No changes.") 49 | self.end_run_loop() 50 | return 51 | chars = chars[:-1] # Chop off newline 52 | for char in chars: 53 | char_index = ord(char) - ord(' ') 54 | print("Current width of %s is %d" % (char, self.font.char_widths[char_index])) 55 | print("Enter new width for characters: ") 56 | width = int(sys.stdin.readline()) 57 | for char in chars: 58 | char_index = ord(char) - ord(' ') 59 | self.font.char_widths[char_index] = width 60 | print("%s => % 2d" % (char, self.font.char_widths[char_index])) 61 | print("Set to %d, text size is now %s" % (width, self.font.size(self.text))) 62 | self.text_layer.set_text(self.text) # Force redrawing the text 63 | self.dirty = True 64 | except Exception as e: 65 | print(e) 66 | 67 | 68 | def tool_populate_options(parser): 69 | pass 70 | 71 | 72 | def tool_get_usage(): 73 | return """ """ 74 | 75 | 76 | def tool_run(options, args): 77 | if len(args) != 2: 78 | return False 79 | 80 | font_path, text = args 81 | 82 | font = dmd.Font(font_path) 83 | if not font: 84 | print("Error loading font") 85 | return False 86 | 87 | print("Enter with no input exits and saves changes.") 88 | game = DmdFontWidthsGame(font, font_path, text) 89 | game.run_loop() 90 | return True 91 | -------------------------------------------------------------------------------- /procgame/tools/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import yaml 5 | 6 | import procgame 7 | import procgame.config 8 | 9 | 10 | def save_config(): 11 | try: 12 | dirname = os.path.dirname(procgame.config.path) 13 | if not os.path.isdir(dirname): 14 | print("Creating directory: %s" % (dirname)) 15 | os.makedirs(dirname) 16 | output = open(procgame.config.path, 'w') 17 | yaml.dump(procgame.config.values, output) 18 | output.close() 19 | del output 20 | except IOError: 21 | print("Error writing to configuration file at " + procgame.config.path) 22 | sys.exit(2) 23 | 24 | 25 | def tool_populate_options(parser): 26 | parser.add_option('-k', '--key', action='store', help='The configuration key to be manipulated.') 27 | parser.add_option('-a', '--add', action='store', help='Add VALUE to list KEY.', metavar='VALUE') 28 | parser.add_option('-r', '--remove', action='store', help='Remove VALUE from list KEY.', metavar='VALUE') 29 | parser.add_option('-s', '--set', action='store', help='Assign the value of KEY to VALUE.', metavar='VALUE') 30 | parser.add_option('-c', '--clear', action='store_true', help='Removes KEY from the configuration.') 31 | 32 | 33 | def tool_get_usage(): 34 | return """[options]""" 35 | 36 | 37 | def tool_run(options, args): 38 | no_values_loaded = (procgame.config.values == {}) 39 | procgame.config.values = procgame.config.values or {} 40 | if options.key: 41 | if options.key not in procgame.config.values and not (options.add or options.set): 42 | print("Key does not exist.") 43 | sys.exit(3) 44 | 45 | if options.add or options.remove: 46 | if options.key in procgame.config.values and type(procgame.config.values[options.key]) != list: 47 | print("Cannot add or remove values from a key that does not have a list value. Type is " + ( 48 | type(procgame.config.values[options.key]).__name__)) 49 | sys.exit(3) 50 | 51 | if options.add: 52 | if options.key not in procgame.config.values: 53 | procgame.config.values[options.key] = [] 54 | procgame.config.values[options.key].append(options.add) 55 | save_config() 56 | return True 57 | if options.remove: 58 | procgame.config.values[options.key].remove(options.remove) 59 | save_config() 60 | return True 61 | 62 | if options.set: 63 | if options.key in procgame.config.values and type(procgame.config.values[options.key]) != str: 64 | print("Cannot assign a value to a key that is not a string. Type is " + ( 65 | type(procgame.config.values[options.key]).__name__)) 66 | sys.exit(3) 67 | 68 | procgame.config.values[options.key] = options.set 69 | save_config() 70 | return True 71 | 72 | if options.clear: 73 | del procgame.config.values[options.key] 74 | save_config() 75 | return True 76 | 77 | print(procgame.config.values[options.key]) 78 | return True 79 | 80 | # If nothing else, show the file location and some diagnostic information: 81 | print("""Your configuration file is located at:""") 82 | print("") 83 | print(""" %s""" % (procgame.config.path)) 84 | print("") 85 | 86 | if not os.path.exists(procgame.config.path): 87 | print('Your configuration file does not exist.') 88 | elif no_values_loaded: 89 | print('Your configuration file contains one or more errors and was not parsed successfully.') 90 | 91 | return True 92 | -------------------------------------------------------------------------------- /procgame/modes/replay.py: -------------------------------------------------------------------------------- 1 | from ..game import Mode 2 | 3 | 4 | class Replay(Mode): 5 | """docstring for AttractMode""" 6 | 7 | def __init__(self, game, priority): 8 | super(Replay, self).__init__(game, priority) 9 | self.replay_achieved = [False, False, False, False] 10 | self.replay_scores = [500000, 600000, 700000, 800000] 11 | self.num_replay_levels = 1 12 | self.replay_callback = 'None' 13 | 14 | def mode_started(self): 15 | 16 | self.replay_type = self.game.user_settings['Replay']['Replay Type'] 17 | if self.replay_type == 'auto': 18 | self.num_levels = 1 19 | else: 20 | self.num_levels = self.game.user_settings['Replay']['Replay Levels'] 21 | self.replay_percentage = float(self.game.user_settings['Replay']['Replay Percentage']) 22 | self.replay_boost = self.game.user_settings['Replay']['Replay Boost'] 23 | self.default_scores = [self.game.user_settings['Replay']['Replay Level 1'], 24 | self.game.user_settings['Replay']['Replay Level 2'], 25 | self.game.user_settings['Replay']['Replay Level 3'], 26 | self.game.user_settings['Replay']['Replay Level 4']] 27 | 28 | self.set_replay_scores() 29 | replay_on = self.replay_type != 'none' 30 | score = self.game.current_player().score 31 | for i in range(0, 4): 32 | # Set already achieved if replay is off, level not active, or 33 | # score is higher 34 | self.replay_achieved[i] = not replay_on or self.num_levels <= i or score > self.replay_scores[i] 35 | for i in range(0, 4): 36 | # Schedule the score check if any level hasn't been achieved yet. 37 | if not self.replay_achieved[i]: 38 | self.delay(name='replay_check', event_type=None, delay=0.3, handler=self.replay_check) 39 | break 40 | 41 | print("Replay scores: %s" % self.replay_scores) 42 | print("Replay achieved: %s" % self.replay_achieved) 43 | print("levels: %s" % self.num_levels) 44 | 45 | def set_replay_scores(self): 46 | if self.replay_type == 'auto': 47 | self.replay_scores[0] = self.calc_auto_replay_score() 48 | if self.replay_scores[0] < self.default_scores[0]: 49 | self.replay_scores[0] = self.default_scores[0] 50 | elif self.replay_type == 'fixed': 51 | for i in range(0, 4): 52 | self.replay_scores[i] = self.default_scores[i] 53 | elif self.replay_type == 'incremental': 54 | self.replay_scores[0] = self.default_scores[0] 55 | for i in range(1, 4): 56 | self.replay_scores[i] = self.replay_scores[i - 1] + self.replay_boost 57 | 58 | def calc_auto_replay_score(self): 59 | return (int( 60 | int(self.game.game_data['Audits']['Avg Score']) * ((100.0 - self.replay_percentage) / 100)) / 10000) * 10000 61 | 62 | def mode_stopped(self): 63 | self.cancel_delayed('replay_check') 64 | 65 | def replay_check(self): 66 | index = 3 67 | for i in range(0, 4): 68 | if not self.replay_achieved[i]: 69 | index = i 70 | break 71 | if not self.replay_achieved[index]: 72 | if self.game.current_player().score > self.replay_scores[index]: 73 | if self.replay_callback != 'None': 74 | self.replay_callback() 75 | self.replay_achieved[index] = True 76 | if self.num_levels > index: 77 | self.delay(name='replay_check', event_type=None, delay=0.3, handler=self.replay_check) 78 | else: 79 | self.delay(name='replay_check', event_type=None, delay=0.3, handler=self.replay_check) 80 | -------------------------------------------------------------------------------- /procgame/tools/dmdconvert.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | import sys 5 | 6 | import procgame.dmd 7 | 8 | logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 9 | 10 | 11 | def load_and_append_image(anim, filename): 12 | if not os.path.exists(filename): 13 | return False 14 | print("Appending", filename) 15 | 16 | tmp = procgame.dmd.Animation().load(filename, allow_cache=False) 17 | if len(tmp.frames) > 0: 18 | first_frame = tmp.frames[0] 19 | anim.width, anim.height = first_frame.width, first_frame.height 20 | anim.frames += tmp.frames 21 | 22 | return True 23 | 24 | 25 | def load_and_append_text(anim, filename, dot_map={'0': 0, '1': 5, '2': 10, '3': 15}): 26 | """Support for text-based DMD files. Each line in the file describes a 27 | row of DMD data, with each character representing a dot. Dot values are 28 | interpreted using the dot_map parameter. A blank line indicates the end 29 | of a frame. 30 | """ 31 | if not os.path.exists(filename): 32 | return False 33 | print("Appending ", filename) 34 | f = open(filename, 'r') 35 | lines = f.readlines() 36 | 37 | # Find the dimensions 38 | w = 0 39 | h = 0 40 | for line in lines: 41 | line = line.strip() 42 | if len(line) == 0: break 43 | w = len(line) 44 | h += 1 45 | print("Dimensions:", w, h) 46 | (anim.width, anim.height) = (w, h) 47 | 48 | frame = procgame.dmd.Frame(w, h) 49 | y = 0 50 | 51 | for line in lines: 52 | 53 | line = line.strip() 54 | 55 | if len(line) == 0: 56 | anim.frames.append(frame) 57 | frame = procgame.dmd.Frame(w, h) 58 | y = 0 59 | continue 60 | 61 | x = 0 62 | for ch in line: 63 | frame.set_dot(x, y, dot_map[ch]) 64 | x += 1 65 | 66 | y += 1 67 | 68 | if y != 0: 69 | anim.frames.append(frame) 70 | 71 | return True 72 | 73 | 74 | def image_to_dmd(src_filenames, dst_filename): 75 | """docstring for image_to_dmd""" 76 | last_filename = None 77 | anim = procgame.dmd.Animation() 78 | 79 | if len(src_filenames) == 1 and re.search("%[0-9]*d", src_filenames[0]): 80 | pattern = src_filenames[0] 81 | src_filenames = [] 82 | for frame_index in range(1000): 83 | src_filenames.append(pattern % (frame_index)) 84 | else: 85 | for filename in src_filenames: 86 | if not os.path.exists(filename): 87 | print('File not found:', filename) 88 | 89 | for filename in src_filenames: 90 | if filename.endswith('.txt'): 91 | load_and_append_text(anim=anim, filename=filename) 92 | else: 93 | load_and_append_image(anim=anim, filename=filename) 94 | 95 | if len(anim.frames) == 0: 96 | print("ERROR: No frames found! Ensure that the source file(s) exist and are readable.") 97 | sys.exit(1) 98 | 99 | anim.save(dst_filename) 100 | print("Saved.") 101 | 102 | 103 | def tool_populate_options(parser): 104 | pass 105 | 106 | 107 | def tool_get_usage(): 108 | return """[options] [... ] 109 | 110 | If only one image name is used it may include %d format specifiers to 111 | create animations. For example, to create an animation of up to 999 112 | frames with sequential names: 113 | 114 | Animation%03d.png Animation.dmd 115 | 116 | Note that in UNIX-like shells that support wildcard expansion you can 117 | enter image*.png as the one image filename and the shell will expend it 118 | to include all filenames matching that wildcard.""" 119 | 120 | 121 | def tool_run(options, args): 122 | if len(args) < 2: 123 | return False 124 | image_to_dmd(src_filenames=args[0:-1], dst_filename=args[-1]) 125 | return True 126 | -------------------------------------------------------------------------------- /procgame/keyboard.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame.locals import * 3 | 4 | from .dmd.dmd import Frame 5 | 6 | pygame.init() 7 | screen_multiplier = 2 8 | screen = pygame.display.set_mode((128 * screen_multiplier, 32 * screen_multiplier)) 9 | pygame.display.set_caption('Press CTRL-C to exit') 10 | rect = pygame.Surface([128 * screen_multiplier, 32 * screen_multiplier]) 11 | 12 | 13 | # circle = pygame.Surface([300, 50]) 14 | # line = pygame.Surface([300,50]) 15 | # color = pygame.Color(100,0,0,100) 16 | # rect = pygame.Surface([128,32]) 17 | # pygame.draw.line(line, color, [0,25], [128,25], 2) 18 | # pygame.draw.circle(circle, color, [125,25], 20,8) 19 | # screen.blit(line, [0,0]) 20 | # pygame.display.flip() 21 | # screen.blit(circle, [0,0]) 22 | # pygame.display.flip() 23 | 24 | class KeyboardHandler(): 25 | """docstring for KeyboardHandler""" 26 | 27 | def __init__(self): 28 | self.ctrl = 0 29 | self.old_frame = Frame(128, 32) 30 | self.old_frame.fill_rect(0, 0, 128, 32, 0) 31 | self.i = 0 32 | 33 | def get_keyboard_events(self): 34 | key_events = [] 35 | for event in pygame.event.get(): 36 | key_event = {} 37 | if event.type == KEYDOWN: 38 | if event.key == K_RCTRL or event.key == K_LCTRL: 39 | self.ctrl = 1 40 | if event.key == K_c: 41 | if self.ctrl == 1: 42 | key_event['type'] = 99 43 | key_event['value'] = 'quit' 44 | elif event.key == K_ESCAPE: 45 | key_event['type'] = 99 46 | key_event['value'] = 'quit' 47 | elif event.key == K_RSHIFT: 48 | key_event['type'] = 1 49 | key_event['value'] = 1 50 | elif event.key == K_LSHIFT: 51 | key_event['type'] = 1 52 | key_event['value'] = 3 53 | elif event.type == KEYUP: 54 | if event.key == K_RCTRL or event.key == K_LCTRL: 55 | self.ctrl = 0 56 | elif event.key == K_RSHIFT: 57 | key_event['type'] = 2 58 | key_event['value'] = 1 59 | elif event.key == K_LSHIFT: 60 | key_event['type'] = 2 61 | key_event['value'] = 3 62 | if len(key_event): 63 | key_events.append(key_event) 64 | return key_events 65 | 66 | def draw(self, frame): 67 | 68 | # Use adjustment to add a one pixel border around each dot, if 69 | # the screen size is large enough to accomodate it. 70 | if screen_multiplier >= 4: 71 | adjustment = -1 72 | else: 73 | adjustment = 0 74 | 75 | # Keep a list of the rectangles (dots) being changed (a dirty list). 76 | # This should increase screen update times. 77 | changed_rect_list = [] 78 | 79 | for y in range(frame.height): 80 | for x in range(frame.width): 81 | 82 | dot = frame.get_dot(x, y) 83 | # if True: 84 | if dot != self.old_frame.get_dot(x, y): 85 | color_val = dot * 16 86 | color = pygame.Color(color_val, color_val, color_val) 87 | 88 | dot = pygame.Rect(x * screen_multiplier, 89 | y * screen_multiplier, 90 | screen_multiplier + adjustment, 91 | screen_multiplier + adjustment) 92 | 93 | pygame.draw.rect(rect, color, dot, 0) 94 | changed_rect_list += [dot] 95 | # pygame.draw.circle(rect, color, [x*screen_multiplier,y*screen_multiplier], screen_multiplier/2, 0) 96 | 97 | screen.blit(rect, [0, 0]) 98 | pygame.display.update(changed_rect_list) 99 | 100 | self.old_frame = frame.copy() 101 | -------------------------------------------------------------------------------- /procgame/modes/ballsearch.py: -------------------------------------------------------------------------------- 1 | from ..game import Mode 2 | 3 | 4 | class BallSearch(Mode): 5 | """Ball Search mode.""" 6 | 7 | def __init__(self, game, priority, countdown_time, coils=[], reset_switches=[], stop_switches=[], 8 | enable_switch_names=[], special_handler_modes=[]): 9 | self.stop_switches = stop_switches 10 | self.countdown_time = countdown_time 11 | self.coils = coils 12 | self.special_handler_modes = special_handler_modes 13 | self.enabled = 0; 14 | Mode.__init__(self, game, 8) 15 | for switch in reset_switches: 16 | self.add_switch_handler(name=str(switch), event_type=str(reset_switches[switch]), delay=None, 17 | handler=self.reset) 18 | # The disable_switch_names identify the switches that, when closed, 19 | # keep the ball search from occuring. This is typically done, 20 | # for instance, when a ball is in the shooter lane or held on a flipper. 21 | for switch in stop_switches: 22 | self.add_switch_handler(name=str(switch), event_type=str(stop_switches[switch]), delay=None, 23 | handler=self.stop) 24 | 25 | def enable(self): 26 | self.enabled = 1; 27 | self.reset('None') 28 | 29 | def disable(self): 30 | self.stop(None) 31 | self.enabled = 0; 32 | 33 | def reset(self, sw): 34 | if self.enabled: 35 | # Stop delayed coil activations in case a ball search has 36 | # already started. 37 | for coil in self.coils: 38 | self.cancel_delayed('ball_search_coil1') 39 | self.cancel_delayed('start_special_handler_modes') 40 | schedule_search = 1 41 | for switch in self.stop_switches: 42 | 43 | # Don't restart the search countdown if a ball 44 | # is resting on a stop_switch. First, 45 | # build the appropriate function call into 46 | # the switch, and then call it using getattr() 47 | sw = self.game.switches[str(switch)] 48 | state_str = str(self.stop_switches[switch]) 49 | m = getattr(sw, 'is_%s' % (state_str)) 50 | if m(): 51 | schedule_search = 0 52 | 53 | if schedule_search: 54 | self.cancel_delayed(name='ball_search_countdown') 55 | self.delay(name='ball_search_countdown', event_type=None, delay=self.countdown_time, 56 | handler=self.perform_search, param=0) 57 | 58 | def stop(self, sw): 59 | self.cancel_delayed(name='ball_search_countdown'); 60 | 61 | def perform_search(self, completion_wait_time, completion_handler=None): 62 | if completion_wait_time != 0: 63 | self.game.set_status("Balls Missing") # Replace with permanent message 64 | delay = .150 65 | for coil in self.coils: 66 | self.delay(name='ball_search_coil1', event_type=None, delay=delay, handler=self.pop_coil, param=str(coil)) 67 | delay = delay + .150 68 | self.delay(name='start_special_handler_modes', event_type=None, delay=delay, 69 | handler=self.start_special_handler_modes) 70 | 71 | if completion_wait_time != 0: 72 | pass 73 | else: 74 | self.cancel_delayed(name='ball_search_countdown') 75 | self.delay(name='ball_search_countdown', event_type=None, delay=self.countdown_time, 76 | handler=self.perform_search, param=0) 77 | 78 | def pop_coil(self, coil): 79 | self.game.coils[coil].pulse() 80 | 81 | def start_special_handler_modes(self): 82 | for special_handler_mode in self.special_handler_modes: 83 | self.game.modes.add(special_handler_mode) 84 | self.delay(name='remove_special_handler_mode', event_type=None, delay=7, 85 | handler=self.remove_special_handler_mode, param=special_handler_mode) 86 | 87 | def remove_special_handler_mode(self, special_handler_mode): 88 | self.game.modes.remove(special_handler_mode) 89 | -------------------------------------------------------------------------------- /procgame/tools/dmdsplashrom.py: -------------------------------------------------------------------------------- 1 | import bz2 2 | import json 3 | import logging 4 | import os 5 | import time 6 | 7 | from io import StringIO 8 | from .mailbox.mailboxclient import MailboxClient 9 | from procgame import dmd 10 | 11 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 12 | 13 | POLL_DELAY = 5 14 | HOST = 'procmailbox000.appspot.com' 15 | PORT = 80 16 | 17 | 18 | # HOST = '127.0.0.1' 19 | # PORT = 8084 20 | 21 | class JobSubmitter(object): 22 | def __init__(self, host, port, api_key): 23 | self.logger = logging.getLogger('worker') 24 | self.client = MailboxClient(host, port, api_key) 25 | 26 | def submit(self, fpga_base, input_path, output_path): 27 | """docstring for run_worker""" 28 | 29 | with open(input_path, 'rb') as f: 30 | job_key = self.client.submit_job(f.read(), json.dumps(fpga_base)) 31 | 32 | if job_key is None: 33 | self.logger.error('Error submitting request.') 34 | return 35 | 36 | self.logger.info('Request submitted; waiting...') 37 | 38 | data = None 39 | 40 | while True: 41 | time.sleep(POLL_DELAY) 42 | self.logger.info('Checking...') 43 | data, status = self.client.poll_for_result(job_key) 44 | if status == 200: 45 | break 46 | elif status == 202: 47 | continue 48 | else: 49 | self.logger.error('Request failed with status %d: %s', status, data) 50 | return 51 | 52 | self.logger.info('Image received!') 53 | 54 | with open(output_path, 'wb') as f: 55 | f.write(bz2.decompress(data)) 56 | 57 | 58 | def tool_get_usage(): 59 | return """[options] 60 | 61 | 62 | Parameters: 63 | key: A transaction key obtained from support@pinballcontrollers.com. 64 | base_fpga_version: Version number of the desired base P-ROC FPGA image. Format is x.yy (ie: 1.18). 65 | file.dmd: Splash screen image in the .dmd format - must be 128x32. 66 | [optional: watermark_x]: Leftmost X coordinate of the P-ROC version watermark. 0 <= x <= 91. 67 | [optional: watermark_y]: Uppermost Y coordinate of the P-ROC version watermark. 0 <= y <= 26. 68 | output.p-roc: Filename for the new FPGA image. Must end in \".p-roc\". 69 | """ 70 | 71 | 72 | def tool_populate_options(parser): 73 | pass 74 | 75 | 76 | def check_params(fpga_base, input_path, output_path): 77 | # Make sure fpga_base is x.yy with digits only 78 | fpga_base_list = str.split(fpga_base['fpga_base'], '.') 79 | if len(fpga_base_list) != 2: return 0 80 | if not fpga_base_list[0].isdigit() or not fpga_base_list[1].isdigit(): return 0 81 | 82 | # Make sure input path is a .dmd filename 83 | if not input_path.endswith(".dmd"): return 0 84 | if not os.path.isfile(input_path): 85 | print("\n\nERROR: Invalid filename: %s" % (input_path)) 86 | return 0 87 | else: 88 | # Check .dmd file dimensions 89 | f = open(input_path, 'rb') 90 | anim = dmd.Animation() 91 | anim.populate_from_dmd_file(StringIO(f.read())) 92 | frame = anim.frames[0] 93 | if frame.width != 128 or frame.height != 32: 94 | print("\n\nERROR: .dmd file must be 128x32") 95 | return 0 96 | 97 | # Make sure requested watermark position is valid 98 | wm_x = fpga_base['wm_x'] 99 | wm_y = fpga_base['wm_y'] 100 | if wm_x < 0 or wm_x > 91 or wm_y < 0 or wm_y > 26: 101 | print("\n\nERROR: Invalid Watermark coordinates.") 102 | print(wm_x) 103 | print(wm_y) 104 | return 0 105 | 106 | # Make sure output path is a .p-roc filename 107 | if not output_path.endswith(".p-roc"): return 0 108 | 109 | 110 | def tool_run(options, args): 111 | if len(args) != 4 and len(args) != 6: 112 | print("\nERROR: Invalid parameters") 113 | tool_get_usage() 114 | else: 115 | api_key = args[0] 116 | input_path = args[2] 117 | fpga_base_dict = {} 118 | if len(args) == 4: 119 | fpga_base_dict['fpga_base'] = args[1] 120 | fpga_base_dict['wm_x'] = 46 121 | fpga_base_dict['wm_y'] = 26 122 | output_path = args[3] 123 | else: 124 | fpga_base_dict['fpga_base'] = args[1] 125 | fpga_base_dict['wm_x'] = int(args[3]) 126 | fpga_base_dict['wm_y'] = int(args[4]) 127 | output_path = args[5] 128 | 129 | if check_params(fpga_base_dict, input_path, output_path) == 0: 130 | print("\nERROR: Invalid parameters") 131 | tool_get_usage() 132 | else: 133 | jobber = JobSubmitter(HOST, PORT, api_key) 134 | jobber.submit(fpga_base_dict, input_path, output_path) 135 | return True 136 | -------------------------------------------------------------------------------- /procgame/modes/ballsave.py: -------------------------------------------------------------------------------- 1 | from ..game import Mode 2 | 3 | 4 | class BallSave(Mode): 5 | """Manages a game's ball save functionality by Keeping track of ball save timer and the number of balls to be saved. 6 | 7 | Parameters: 8 | 9 | 'game': Parent game object. 10 | 'lamp": Name of lamp to blink while ball save is active. 11 | 'delayed_start_switch': Optional - Name of switch who's inactive event will cause the ball save timer to start (ie. Shooter Lane). 12 | """ 13 | 14 | def __init__(self, game, lamp, delayed_start_switch='None'): 15 | super(BallSave, self).__init__(game, 3) 16 | self.lamp = lamp 17 | self.num_balls_to_save = 1 18 | self.mode_begin = 0 19 | self.allow_multiple_saves = False 20 | self.timer = 0 21 | if delayed_start_switch != 'None' and delayed_start_switch != 'none': 22 | self.add_switch_handler(name=delayed_start_switch, event_type='inactive', delay=1.0, 23 | handler=self.delayed_start_handler) 24 | 25 | """ Optional method to be called when a ball is saved. Should be defined externally.""" 26 | self.callback = None 27 | 28 | """ Optional method to be called to tell a trough to save balls. Should be linked externally to an enable method for a trough.""" 29 | self.trough_enable_ball_save = None 30 | 31 | def mode_stopped(self): 32 | self.disable() 33 | 34 | def launch_callback(self): 35 | """Disables the ball save logic when multiple saves are not allowed. This is typically linked to a Trough object so the trough can notify this logic when a ball is being saved. If 'self.callback' is externally defined, that method will be called from here.""" 36 | if not self.allow_multiple_saves: 37 | self.disable() 38 | if self.callback: 39 | self.callback() 40 | 41 | def start_lamp(self): 42 | """Starts blinking the ball save lamp. Oftentimes called externally to start blinking the lamp before a ball is plunged.""" 43 | self.lamp.schedule(schedule=0xFF00FF00, cycle_seconds=0, now=True) 44 | 45 | def update_lamps(self): 46 | if self.timer > 5: 47 | self.lamp.schedule(schedule=0xFF00FF00, cycle_seconds=0, now=True) 48 | elif self.timer > 2: 49 | self.lamp.schedule(schedule=0x55555555, cycle_seconds=0, now=True) 50 | else: 51 | self.lamp.disable() 52 | 53 | def add(self, add_time, allow_multiple_saves=True): 54 | """Adds time to the ball save timer.""" 55 | if self.timer >= 1: 56 | self.timer += add_time 57 | self.update_lamps() 58 | else: 59 | self.start(self.num_balls_to_save, add_time, True, allow_multiple_saves) 60 | 61 | def disable(self): 62 | """Disables the ball save logic.""" 63 | if self.trough_enable_ball_save: 64 | self.trough_enable_ball_save(False) 65 | self.timer = 0 66 | self.lamp.disable() 67 | 68 | # self.callback = None 69 | 70 | def start(self, num_balls_to_save=1, time=12, now=True, allow_multiple_saves=False): 71 | """Activates the ball save logic.""" 72 | self.allow_multiple_saves = allow_multiple_saves 73 | self.num_balls_to_save = num_balls_to_save 74 | if time > self.timer: self.timer = time 75 | self.update_lamps() 76 | if now: 77 | self.cancel_delayed('ball_save_timer') 78 | self.delay(name='ball_save_timer', event_type=None, delay=1, handler=self.timer_countdown) 79 | if self.trough_enable_ball_save: 80 | self.trough_enable_ball_save(True) 81 | else: 82 | self.mode_begin = 1 83 | self.timer_hold = time 84 | 85 | def timer_countdown(self): 86 | self.timer -= 1 87 | if self.timer >= 1: 88 | self.delay(name='ball_save_timer', event_type=None, delay=1, handler=self.timer_countdown) 89 | else: 90 | self.disable() 91 | 92 | self.update_lamps() 93 | 94 | def is_active(self): 95 | return self.timer > 0 96 | 97 | def get_num_balls_to_save(self): 98 | """Returns the number of balls that can be saved. Typically this is linked to a Trough object so the trough can decide if a a draining ball should be saved.""" 99 | return self.num_balls_to_save 100 | 101 | def saving_ball(self): 102 | if not self.allow_multiple_saves: 103 | self.timer = 1 104 | self.lamp.disable() 105 | 106 | def delayed_start_handler(self, sw): 107 | if self.mode_begin: 108 | self.timer = self.timer_hold 109 | self.mode_begin = 0 110 | self.update_lamps() 111 | self.cancel_delayed('ball_save_timer') 112 | self.delay(name='ball_save_timer', event_type=None, delay=1, handler=self.timer_countdown) 113 | if self.trough_enable_ball_save: 114 | self.trough_enable_ball_save(True) 115 | -------------------------------------------------------------------------------- /procgame/dmd/markup.py: -------------------------------------------------------------------------------- 1 | from procgame.dmd import Frame, font_named 2 | 3 | 4 | class MarkupFrameGenerator: 5 | """Renders a :class:`~procgame.dmd.Frame` for given text-based markup. 6 | 7 | The markup format presently uses three markup tokens: 8 | ``#`` (for headlines) and ``[`` and ``]`` for plain text. The markup tokens 9 | indicate justification. Lines with no markup or a leading ``#`` or ``[`` 10 | will be left-justified. Lines with a trailing ``#`` or ``]`` will be right- 11 | justified. Lines with both will be centered. 12 | 13 | The width and min_height are specified with instantiation. 14 | 15 | Fonts can be adjusted by assigning the :attr:`font_plain` and :attr:`font_bold` member variables. 16 | """ 17 | 18 | font_plain = None 19 | """Font used for plain, non-bold characters.""" 20 | font_bold = None 21 | """Font used for bold characters.""" 22 | 23 | def __init__(self, width=128, min_height=32): 24 | self.width = width 25 | self.min_height = min_height 26 | self.frame = None 27 | self.font_plain = font_named('Font07x5.dmd') 28 | self.font_bold = font_named('Font09Bx7.dmd') 29 | 30 | def frame_for_markup(self, markup, y_offset=0): 31 | """Returns a Frame with the given markup rendered within it. 32 | The frame width is fixed, but the height will be adjusted 33 | to fit the contents while respecting min_height. 34 | 35 | The Y offset can be configured supplying *y_offset*. 36 | """ 37 | lines = markup.split('\n') 38 | for draw in [False, True]: 39 | y = y_offset 40 | for line in lines: 41 | if line.startswith('#') and line.endswith('#'): # centered headline! 42 | y = self.__draw_text(y=y, text=line[1:-1], font=self.font_bold, justify='center', draw=draw) 43 | elif line.startswith('#'): # left-justified headline 44 | y = self.__draw_text(y=y, text=line[1:], font=self.font_bold, justify='left', draw=draw) 45 | elif line.endswith('#'): # right-justified headline 46 | y = self.__draw_text(y=y, text=line[:-1], font=self.font_bold, justify='right', draw=draw) 47 | elif line.startswith('[') and line.endswith(']'): # centered text 48 | y = self.__draw_text(y=y, text=line[1:-1], font=self.font_plain, justify='center', draw=draw) 49 | elif line.endswith(']'): # right-justified text 50 | y = self.__draw_text(y=y, text=line[:-1], font=self.font_plain, justify='right', draw=draw) 51 | elif line.startswith('['): # left-justified text 52 | y = self.__draw_text(y=y, text=line[1:], font=self.font_plain, justify='left', draw=draw) 53 | else: # left-justified but nothing to clip off 54 | y = self.__draw_text(y=y, text=line, font=self.font_plain, justify='left', draw=draw) 55 | if not draw: # this was a test run to get the height 56 | self.frame = Frame(width=self.width, height=max(self.min_height, y)) 57 | return self.frame 58 | 59 | def __draw_text(self, y, text, font, justify, draw): 60 | if max(font.char_widths) * len(text) > self.width: 61 | # Need to do word-wrapping! 62 | line = '' 63 | w = 0 64 | for ch in text: 65 | line += ch 66 | w += font.size(ch)[0] 67 | if w > self.width: 68 | # Too much! We need to back-track for the last space, if possible.. 69 | idx = line.rfind(' ') 70 | if idx == -1: 71 | # No space; we'll have to break before this char and continue. 72 | y = self.__draw_line(y=y, text=line[:-1], font=font, justify=justify, draw=draw) 73 | line = ch 74 | else: 75 | # We have found a space! 76 | y = self.__draw_line(y=y, text=line[:idx], font=font, justify=justify, draw=draw) 77 | line = line[idx + 1:] 78 | # Recalculate w. 79 | w = font.size(line)[0] 80 | if len(line) > 0: # leftover text we need to draw 81 | y = self.__draw_line(y=y, text=line, font=font, justify=justify, draw=draw) 82 | return y 83 | else: 84 | return self.__draw_line(y=y, text=text, font=font, justify=justify, draw=draw) 85 | 86 | def __draw_line(self, y, text, font, justify, draw): 87 | """Draw a line without concern for word-wrapping.""" 88 | if draw: 89 | x = 0 # TODO: x should be set based on 'justify'. 90 | if justify != 'left': 91 | w = font.size(text)[0] 92 | if justify == 'center': 93 | x = (self.frame.width - w) / 2 94 | else: 95 | x = (self.frame.width - w) 96 | font.draw(frame=self.frame, text=text, x=x, y=y) 97 | y += font.char_size 98 | return y 99 | -------------------------------------------------------------------------------- /docs/sphinx/tools.rst: -------------------------------------------------------------------------------- 1 | procgame Command Line Tool 2 | ========================== 3 | 4 | pyprocgame includes a number of tools to make certain tasks easier. Your installation should have included a ``procgame`` command line tool. Once ``procgame`` is in your path (the steps for this will depend on your specific platform; on some platforms it will already be in your path), you can invoke it on the command line:: 5 | 6 | $ procgame 7 | Usage: procgame ... 8 | 9 | Commands: 10 | config Configuration tool. 11 | dmdconvert Converts image files to .dmd files. 12 | dmdfontwidths Interactively assign font width values. 13 | dmdplayer Play a .dmd file. 14 | dmdsplashrom Create a P-ROC ROM with a custom power-up image. 15 | lampshow Play a lamp show. 16 | 17 | Without any arguments, ``procgame`` shows the available commands within the tool. Run ``procgame`` again with the command specified in order to see information about that command:: 18 | 19 | $ procgame dmdconvert 20 | Usage: procgame dmdconvert [options] [... ] 21 | 22 | If only one image name is used it may include %d format specifiers to 23 | ... 24 | ... 25 | 26 | The following documents the specifics of some of the ``procgame`` tools. 27 | 28 | 29 | .. _tool-config: 30 | 31 | config 32 | ------ 33 | 34 | The ``config`` tool assists in managing the pyprocgame configuration file, located at ``~/.pyprocgame/config.yaml``. Run it without any arguments to see the location of your config.yaml file:: 35 | 36 | $ procgame config 37 | Your configuration file is located at: 38 | 39 | /home/me/.pyprocgame/config.yaml 40 | 41 | You can assign string values in your configuration using the ``--set`` option:: 42 | 43 | $ procgame config --key=pinproc_class --set=procgame.fakepinproc.FakePinPROC 44 | 45 | Or clear them with ``--clear``:: 46 | 47 | $ procgame config --key=pinproc_class --clear 48 | 49 | You can also use ``config`` to manage your ``font_path``:: 50 | 51 | $ procgame config --key=font_path --add="/home/me/dmd_fonts" 52 | 53 | Run ``procgame config --help`` to view other options. 54 | 55 | 56 | .. _tool-dmdconvert: 57 | 58 | dmdconvert 59 | ---------- 60 | 61 | Use ``dmdconvert`` to convert one or more image files into a 16-color ``.dmd`` animation file, which can later be loaded by :meth:`procgame.dmd.Animation.load`. Its usage is as follows:: 62 | 63 | procgame dmdconvert [... ] 64 | 65 | If only one image name is supplied, the ``%d`` format specifier may be used to iterate over image files matching a pattern:: 66 | 67 | procgame dmdconvert Animation%03d.png Animation.dmd 68 | 69 | Additionally, UNIX shells with wildcard expansion support allow the following:: 70 | 71 | procgame dmdconvert Animation*.png Animation.dmd 72 | 73 | ``dmdconvert`` requires the Python Imaging Library (PIL). It does not require that the P-ROC hardware be installed. 74 | 75 | 76 | dmdfontwidths 77 | ------------- 78 | 79 | ``dmdfontwidths`` provides an interactive, text-based interface for specifying the font widths of individual characters in a ``.dmd`` font file. Its usage is as follows:: 80 | 81 | procgame dmdfontwidths 82 | 83 | The provided text will be displayed using the P-ROC hardware in the given font file, which may be a single-frame ``.dmd``; if it is a second frame will be added to contain the font widths (this is a feature of :meth:`procgame.dmd.Font.load`). 84 | 85 | To assign character widths, enter the character(s) you wish to change and press return. Then type the width to assign to all of the specified characters at once. The DMD will be updated with the new font width values. Repeat this process until the font widths of the given text are to your liking. Hit return with a blank line to exit. 86 | 87 | Tips: 88 | 89 | * All characters in a new font will have a zero width; as such there will likely be nothing shown on the DMD. 90 | * Given the limited width of the DMD you will likely need to use several text strings to configure all of the characters in the font. 91 | 92 | 93 | dmdplayer 94 | --------- 95 | 96 | ``dmdplayer`` displays a ``.dmd`` file on the DMD. Its usage is as follows:: 97 | 98 | procgame dmdplayer 99 | 100 | 101 | .. _tool-dmdsplashrom: 102 | 103 | dmdsplashrom 104 | ------------ 105 | 106 | ``dmdsplashrom`` requests a new P-ROC ROM image (.p-roc file) with a custom power-up image. Usage:: 107 | 108 | procgame dmdsplashrom 109 | 110 | ``key`` 111 | A transaction key obtained from support@pinballcontrollers.com. 112 | 113 | ``base_fpga_version`` 114 | Version number of the desired base P-ROC image. Format is x.yy (ie: 1.18). 115 | 116 | ``file.dmd`` 117 | Splash screen image in the .dmd format - must be a single frame at 128x32. 118 | 119 | ``output.p-roc`` 120 | Filename for the new P-ROC image. Must end in ".p-roc". 121 | 122 | All images made with this utility will have a P-ROC watermark applied, showing 'P-ROC' and the image version number. 123 | -------------------------------------------------------------------------------- /docs/sphinx/notes/2010-01.rst: -------------------------------------------------------------------------------- 1 | pyprocgame 0.9 Release Notes 2 | ============================ 3 | 4 | The following change notes are a summary of changes between git hash 5 | `8482d6009a3b81f8624e `_ and 6 | `8a769c077d1c184f5dcd `_: 7 | 8 | General & Game 9 | -------------- 10 | 11 | - Reorganized pyprocgame module source into separate files but consolidated submodules. Most classes are still at the same module path externally, but the following have moved: 12 | 13 | - :class:`procgame.scoredisplay.ScoreDisplay` moved to :class:`procgame.modes.ScoreDisplay` 14 | - :class:`procgame.trough.Trough` moved to :class:`procgame.modes.Trough` 15 | - :class:`procgame.ballsave.BallSave` moved to :class:`procgame.modes.BallSave` 16 | - :class:`procgame.ballsearch.BallSearch` moved to :class:`procgame.modes.BallSearch` 17 | - :class:`procgame.replay.Replay` moved to :class:`procgame.modes.Replay` 18 | - :class:`procgame.highscoreentry.HighScoreEntry` moved to :class:`procgame.modes.HighScoreEntry` 19 | 20 | Most of the changes can be seen in commit `c06a031d1505c30a86b4 `_. 21 | 22 | - :class:`~procgame.game.Mode` switch handlers should now return :data:`procgame.game.SwitchStop` or :data:`~procgame.game.SwitchContinue`, instead of `True` or `False`. Although the values are the same the meaning is much clearer. 23 | 24 | - Added :class:`procgame.game.BasicGame`, a new subclass of :class:`procgame.game.GameController` that comes with helpful classes like :class:`procgame.modes.ScoreDisplay` already installed. Recommended for new game development. 25 | 26 | - Added :mod:`procgame.config` module to facilitate system-wide configuration such as the path to DMD files (used to populate :attr:`procgame.dmd.font_path` and used within :meth:`procgame.dmd.font_named`). 27 | 28 | - Added :class:`procgame.desktop.Desktop` which provides a virtual DMD and incorporates keyboard-to-event features previously in :mod:`procgame.keyboard`. Both are demonstrated in the source code to :class:`procgame.game.BasicGame`. 29 | 30 | - Removed :attr:`procgame.game.Player.info_record`. Devs should use a game-specific subclass of :class:`Player` with the new method :meth:`procgame.game.GameController.create_player`. 31 | 32 | - Added :class:`procgame.fakepinproc.FakePinPROC`, a :class:`pinproc.PinPROC` stand-in. 33 | 34 | - Added :meth:`procgame.game.GameController.create_pinproc` to enable subclassing or replacing :class:`pinproc.PinPROC`. 35 | 36 | - The :class:`procgame.sound.SoundController` will disable itself if it cannot initialize :mod:`pygame.mixer`. 37 | 38 | - Added :meth:`procgame.game.GameController.get_events` and :meth:`procgame.game.GameController.tick`. 39 | 40 | - Renamed *machineType* variables to the more Pythonic *machine_type*. 41 | 42 | - Renamed :meth:`procgame.game.GameController.write_settings` to :meth:`~procgame.game.GameController.save_settings`. Renamed :meth:`~procgame.game.GameController.write_game_data` to :meth:`~procgame.game.GameController.save_game_data`. 43 | 44 | DMD 45 | --- 46 | 47 | - Fixed :meth:`procgame.dmd.Animation.save` on some Windows platforms. 48 | 49 | - Removed :attr:`procgame.dmd.DisplayController.capture` and :attr:`procgame.dmd.DisplayController.alt_frame_handler` in favor of :attr:`procgame.dmd.DisplayController.frame_handlers`. 50 | 51 | - Added :meth:`procgame.dmd.Frame.subframe` and :meth:`procgame.dmd.Frame.create_with_text`. 52 | 53 | - Fixed a case where the :attr:`procgame.dmd.Layer.opaque` property was not checked by :meth:`procgame.dmd.DisplayController.update`. 54 | 55 | - Added :meth:`procgame.dmd.ScriptedLayer.force_next`. Added transitions to :class:`procgame.dmd.ScriptedLayer` (experimental). 56 | 57 | - :class:`procgame.dmd.AnimatedLayer` changed to no longer destructively edit the frames list. Added :class:`procgame.dmd.FrameQueueLayer`, which does destructively edit the frames list. 58 | 59 | - dmdconvert.py now sets upper four bits of each DMD dot to the source image's alpha channel value, if present. 60 | 61 | - Added :mod:`procgame.highscore` module, with new :class:`procgame.highscore.EntrySequenceManager`. 62 | 63 | - Added `y_offset` parameter to :meth:`procgame.dmd.MarkupFrameGenerator.frame_for_markup`. 64 | 65 | - Added :class:`procgame.dmd.PanningLayer`. 66 | 67 | - Added support for coils to :class:`procgame.lamps.LampShowTrack` and fixed a bug (thanks Koen) in name processing. 68 | 69 | pypinproc 70 | --------- 71 | 72 | - :class:`procgame.game.GameController` now calls :meth:`pinproc.PinPROC.flush` after every game loop. This is in response to removing :func:`PRFlush` calls from within pypinproc driver update calls. 73 | 74 | - Added event type constants :attr:`pinproc.EventTypeSwitchClosedDebounced` and others. 75 | 76 | - Added machine type constants :attr:`pinproc.MachineTypeWPC` and others. Added support for ``wpc95`` and ``wpcAlphanumeric`` machine types. 77 | 78 | - Added auxiliary bus commands. 79 | 80 | - Added :meth:`pinproc.PinPROC.driver_pulsed_patter`. 81 | 82 | - Added switch rule reload; see :meth:`pinproc.PinPROC.switch_update_rule`. 83 | 84 | -------------------------------------------------------------------------------- /procgame/highscore/category.py: -------------------------------------------------------------------------------- 1 | from .sequence import * 2 | 3 | 4 | class HighScoreCategory: 5 | game_data_key = None 6 | """Key to this high score category's data within :attr:`game.GameController.game_data`.""" 7 | 8 | scores = None 9 | """List of :class:`HighScore` objects for this category. Populated by :meth:`load_from_game`.""" 10 | 11 | score_suffix_singular = '' 12 | """Singular suffix to append to string representations of high scores in this category, such as ``point``. 13 | Used by :func:`generate_highscore_frames`.""" 14 | score_suffix_plural = '' 15 | """Plural suffix to append to string representations of high scores in this category, such as ``points``. 16 | Used by :func:`generate_highscore_frames`.""" 17 | 18 | score_for_player = None 19 | """Method used to fetch the high score *score* value for a given :class:`~procgame.game.Player`. 20 | The default value is:: 21 | 22 | lambda player: player.score 23 | 24 | """ 25 | 26 | titles = ['Grand Champion', 'High Score #1', 'High Score #2', 'High Score #3', 'High Score #4'] 27 | """There must be a title for each high score slot desired for this category.""" 28 | 29 | def __init__(self): 30 | self.score_for_player = lambda player: player.score 31 | 32 | def load_from_game(self, game): 33 | """Loads :attr:`scores` from *game* using :attr:`game_data_key`.""" 34 | if self.game_data_key in game.game_data: 35 | self.scores = list() 36 | for d in game.game_data[self.game_data_key]: 37 | self.scores.append(HighScore().from_dict(d)) 38 | else: 39 | game.logger.warning('HighScoreCategory.load_from_game(): game_data_key %s not found in game_data.', 40 | self.game_data_key) 41 | 42 | for score in self.scores: 43 | score.key = None # No key for existing scores. 44 | 45 | def save_to_game(self, game): 46 | """Saves :attr:`scores` to *game* using :attr:`game_data_key`.""" 47 | save_scores = map(lambda s: s.to_dict(), self.scores) 48 | game.game_data[self.game_data_key] = save_scores 49 | 50 | 51 | class CategoryDrivenDataHelper: 52 | """Utility class used by :class:`CategoryLogic`.""" 53 | 54 | game = None 55 | 56 | categories = None 57 | 58 | def __init__(self, game, categories): 59 | self.game = game 60 | self.categories = categories 61 | self.load_from_game_data() 62 | 63 | def load_from_game_data(self): 64 | for category in self.categories: 65 | category.load_from_game(self.game) 66 | 67 | def save_to_game_data(self): 68 | for category in self.categories: 69 | category.save_to_game(self.game) 70 | 71 | def add_placeholder(self, category, score, name): 72 | """Uses the name as the key.""" 73 | hs = HighScore(score=score, inits=None, name=name, key=name) 74 | category.scores.append(hs) 75 | category.scores = sorted(category.scores, reverse=True) # Reverse to sort from high to low. 76 | category.scores = category.scores[0:len(category.titles)] 77 | 78 | def prompts(self): 79 | prompts = list() 80 | # Create keyed_prompts: 81 | keyed_prompts = {} 82 | for category in self.categories: 83 | for index, score in enumerate(category.scores): 84 | if score.inits is None: 85 | new_title = category.titles[index] 86 | if score.key in keyed_prompts: 87 | existing = keyed_prompts[score.key] 88 | existing.right.append(new_title) 89 | else: 90 | keyed_prompts[score.key] = EntryPrompt(left=score.name, right=[new_title]) 91 | # Process keyed_prompts into prompts: 92 | for key in keyed_prompts: 93 | prompt = keyed_prompts[key] 94 | prompt.key = key 95 | prompts.append(prompt) 96 | return prompts 97 | 98 | def set_inits_by_key(self, key, inits): 99 | for category in self.categories: 100 | for score in category.scores: 101 | if score.key == key: 102 | score.inits = inits 103 | 104 | 105 | class CategoryLogic(HighScoreLogic): 106 | """Subclass of :class:`HighScoreLogic`. Implements a variable number of scoreboards using categories. 107 | 108 | *categories* is a list of :class:`HighScoreCategory` instances which will be checked for 109 | qualifying high scores. 110 | """ 111 | 112 | game = None 113 | data = None 114 | categories = None 115 | 116 | def __init__(self, game, categories): 117 | self.game = game 118 | self.categories = categories 119 | 120 | def prompts(self): 121 | self.data = CategoryDrivenDataHelper(game=self.game, categories=self.categories) 122 | for category in self.categories: 123 | for player in self.game.players: 124 | self.data.add_placeholder(category=category, score=category.score_for_player(player), name=player.name) 125 | return self.data.prompts() 126 | 127 | def store_initials(self, key, inits): 128 | self.data.set_inits_by_key(key=key, inits=inits) 129 | self.data.save_to_game_data() 130 | -------------------------------------------------------------------------------- /procgame/tools/mailbox/mailboxclient.py: -------------------------------------------------------------------------------- 1 | import json # note: requires Python 2.6 2 | import sys 3 | import urllib.parse 4 | 5 | from http.client import HTTPConnection 6 | from .clientutil import encode_multipart_formdata 7 | 8 | 9 | # from django.utils import simplejson as json 10 | 11 | class MailboxClient: 12 | def __init__(self, host, port, api_key): 13 | self.host = host 14 | self.port = port 15 | self.api_key = api_key 16 | 17 | def connection(self): 18 | return HTTPConnection(self.host, self.port) 19 | 20 | def submit_job(self, dmd_data, fpga_base): 21 | """docstring for submit_job""" 22 | 23 | fields = [('api_key', self.api_key), ('base', fpga_base)] 24 | files = [('data', 'data.dmd', dmd_data)] 25 | content_type, body = encode_multipart_formdata(fields, files) 26 | headers = {"Content-type": content_type, "Accept": "text/plain"} 27 | 28 | # print "\n", body, "\n" 29 | 30 | conn = self.connection() 31 | conn.request('POST', '/jobs/new', body, headers) 32 | resp = conn.getresponse() 33 | resp_data = resp.read() 34 | conn.close() 35 | if resp.status == 200: 36 | return resp_data 37 | else: 38 | print(resp.status, resp.reason) 39 | print(resp_data) 40 | return None 41 | 42 | def poll_for_result(self, job_key): 43 | 44 | params = {'api_key': self.api_key} 45 | 46 | conn = self.connection() 47 | conn.request('GET', '/jobs/%s/result?%s' % (job_key, urllib.parse.urlencode(params))) 48 | resp = conn.getresponse() 49 | resp_data = resp.read() 50 | conn.close() 51 | return resp_data, resp.status 52 | 53 | # Worker Requests 54 | 55 | def list_jobs(self, timestamp): 56 | 57 | params = {'api_key': self.api_key, 'timestamp': timestamp} 58 | 59 | conn = self.connection() 60 | conn.request('GET', '/jobs/list?' + urllib.parse.urlencode(params)) 61 | resp = conn.getresponse() 62 | resp_data = resp.read() 63 | conn.close() 64 | if resp.status == 200: 65 | jobs_arr = json.loads(resp_data) 66 | return jobs_arr 67 | else: 68 | print(resp.status, resp.reason) 69 | print(resp_data) 70 | return None 71 | 72 | def get_job_input(self, job_key): 73 | params = {'api_key': self.api_key} 74 | conn = self.connection() 75 | conn.request('GET', '/jobs/%s/request?%s' % (job_key, urllib.parse.urlencode(params))) 76 | resp = conn.getresponse() 77 | resp_data = resp.read() 78 | conn.close() 79 | if resp.status == 200: 80 | return resp_data 81 | else: 82 | print(resp.status, resp.reason) 83 | print(resp_data) 84 | return None 85 | 86 | def submit_result(self, job_key, status_code, result_content_type, result_data): 87 | 88 | fields = [('api_key', self.api_key), ('status_code', str(status_code)), ('content_type', result_content_type)] 89 | files = [('data', 'filename', result_data)] 90 | content_type, body = encode_multipart_formdata(fields, files) 91 | headers = {"Content-type": content_type, "Accept": "text/plain"} 92 | 93 | # print "\n", body, "\n" 94 | 95 | conn = self.connection() 96 | conn.request('POST', '/jobs/%s/result' % (job_key), body, headers) 97 | resp = conn.getresponse() 98 | resp_data = resp.read() 99 | conn.close() 100 | if resp.status == 200: 101 | return resp_data 102 | else: 103 | print(resp.status, resp.reason) 104 | print(resp_data) 105 | return None 106 | 107 | 108 | def main(): 109 | command = sys.argv[1] 110 | api_key = sys.argv[2] 111 | client = MailboxClient(host='127.0.0.1', port=8084, api_key=api_key) 112 | 113 | if command == 'submit': 114 | dmd_path = sys.argv[3] 115 | fpga_base = sys.argv[4] 116 | # wm_x = sys.argv[5] 117 | # wm_y = sys.argv[6] 118 | 119 | with open(dmd_path, 'rb') as f: 120 | dmd_data = f.read() 121 | 122 | print('length:', len(dmd_data)) 123 | 124 | # job_key = client.submit_job(dmd_data, fpga_base, wm_x, wm_y) 125 | job_key = client.submit_job(dmd_data, fpga_base) 126 | print('Got job key:', job_key) 127 | 128 | elif command == 'list': 129 | timestamp = sys.argv[3] 130 | jobs = client.list_jobs(timestamp) 131 | for job in jobs: 132 | print(job['timestamp'], job['job_key']) 133 | 134 | elif command == 'get_result': 135 | job_key = sys.argv[3] 136 | data = client.poll_for_result(job_key) 137 | if data: 138 | print('get_result got %d bytes' % len(data)) 139 | else: 140 | print('no result') 141 | 142 | elif command == 'put_result': 143 | job_key = sys.argv[3] 144 | status_code = sys.argv[4] 145 | content_type = sys.argv[5] 146 | filename = sys.argv[6] 147 | with open(filename, 'rb') as f: 148 | client.submit_result(job_key, status_code, content_type, f.read()) 149 | 150 | else: 151 | print('Unrecognized command.') 152 | 153 | 154 | if __name__ == "__main__": 155 | main() 156 | -------------------------------------------------------------------------------- /procgame/sound.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | import time 4 | 5 | try: 6 | logging.getLogger('game.sound').info("Initializing sound...") 7 | from pygame import mixer # This call takes a while. 8 | except ImportError as e: 9 | logging.getLogger('game.sound').error("Error importing pygame.mixer; sound will be disabled! Error: " + str(e)) 10 | 11 | import os.path 12 | 13 | 14 | class SoundController(object): 15 | """Wrapper for pygame sound.""" 16 | 17 | enabled = True 18 | 19 | def __init__(self, delegate): 20 | super(SoundController, self).__init__() 21 | self.logger = logging.getLogger('game.sound') 22 | try: 23 | mixer.init() 24 | except Exception as e: 25 | # The import mixer above may work, but init can still fail if mixer is not fully supported. 26 | self.enabled = False 27 | self.logger.error("pygame mixer init failed; sound will be disabled: " + str(e)) 28 | self.sounds = {} 29 | self.music = {} 30 | self.music_volume_offset = 0 31 | self.set_volume(0.5) 32 | self.voice_end_time = 0 33 | 34 | def play_music(self, key, loops=0, start_time=0.0): 35 | """Start playing music at the given *key*.""" 36 | if not self.enabled: return 37 | if key in self.music: 38 | if len(self.music[key]) > 0: 39 | random.shuffle(self.music[key]) 40 | self.load_music(key) 41 | mixer.music.play(loops, start_time) 42 | 43 | def stop_music(self): 44 | """Stop the currently-playing music.""" 45 | if not self.enabled: return 46 | mixer.music.stop() 47 | 48 | def fadeout_music(self, time_ms=450): 49 | """ """ 50 | if not self.enabled: return 51 | mixer.music.fadeout(time_ms) 52 | 53 | def load_music(self, key): 54 | """ """ 55 | if not self.enabled: return 56 | mixer.music.load(self.music[key][0]) 57 | 58 | def register_sound(self, key, sound_file): 59 | """ """ 60 | self.logger.info("Registering sound - key: %s, file: %s", key, sound_file) 61 | if not self.enabled: return 62 | if os.path.isfile(sound_file): 63 | self.new_sound = mixer.Sound(str(sound_file)) 64 | self.new_sound.set_volume(self.volume) 65 | if key in self.sounds: 66 | if not self.new_sound in self.sounds[key]: 67 | self.sounds[key].append(self.new_sound) 68 | else: 69 | self.sounds[key] = [self.new_sound] 70 | else: 71 | self.logger.error("Sound registration error: file %s does not exist!", sound_file) 72 | 73 | def register_music(self, key, music_file): 74 | """ """ 75 | if not self.enabled: return 76 | if os.path.isfile(music_file): 77 | if key in self.music: 78 | if not music_file in self.music[key]: 79 | self.music[key].append(music_file) 80 | else: 81 | self.music[key] = [music_file] 82 | else: 83 | self.logger.error("Music registration error: file %s does not exist!", music_file) 84 | 85 | def play(self, key, loops=0, max_time=0, fade_ms=0): 86 | """ """ 87 | if not self.enabled: return 88 | if key in self.sounds: 89 | if len(self.sounds[key]) > 0: 90 | random.shuffle(self.sounds[key]) 91 | self.sounds[key][0].play(loops, max_time, fade_ms) 92 | return self.sounds[key][0].get_length() 93 | else: 94 | return 0 95 | 96 | def play_voice(self, key, loops=0, max_time=0, fade_ms=0): 97 | """ """ 98 | if not self.enabled: return 0 99 | current_time = time.time() 100 | 101 | # Make sure previous voice call is finished. 102 | if current_time < self.voice_end_time: return 0 103 | if key in self.sounds: 104 | if len(self.sounds[key]) > 0: 105 | random.shuffle(self.sounds[key]) 106 | self.sounds[key][0].play(loops, max_time, fade_ms) 107 | duration = self.sounds[key][0].get_length() * (loops + 1) 108 | self.voice_end_time = current_time + duration 109 | return duration 110 | else: 111 | return 0 112 | 113 | def stop(self, key, loops=0, max_time=0, fade_ms=0): 114 | """ """ 115 | if not self.enabled: return 116 | if key in self.sounds: 117 | self.sounds[key][0].stop() 118 | 119 | def volume_up(self): 120 | """ """ 121 | if not self.enabled: return 122 | if self.volume < 0.8: 123 | self.set_volume(self.volume + 0.1) 124 | return self.volume * 10 125 | 126 | def volume_down(self): 127 | """ """ 128 | if not self.enabled: return 129 | if self.volume > 0.2: 130 | self.set_volume(self.volume - 0.1) 131 | return self.volume * 10 132 | 133 | def set_volume(self, new_volume): 134 | """ """ 135 | if not self.enabled: return 136 | self.volume = new_volume 137 | mixer.music.set_volume(new_volume + self.music_volume_offset) 138 | for key in self.sounds: 139 | for sound in self.sounds[key]: 140 | sound.set_volume(self.volume) 141 | 142 | def beep(self): 143 | if not self.enabled: return 144 | pass 145 | -------------------------------------------------------------------------------- /docs/sphinx/ref/highscore.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | highscore Submodule 3 | ******************* 4 | 5 | .. module:: procgame.highscore 6 | 7 | Overview 8 | ======== 9 | 10 | The highscore module provides a set of classes to make gathering and displaying high score information relatively simple. Classic Grand Champion, High Score #1-#4 style high score tables can be created with just a few lines. With a few more lines you can gather more sophisticated high scores, such as loop champion or similar. 11 | 12 | While :class:`InitialEntryMode` prompts the player for their initials, most developers will want to use :class:`EntrySequenceManager`, which coordinates the display of a series of :class:`InitialEntryMode` s. :class:`EntrySequenceManager` is designed to be used with a subclass of :class:`HighScoreLogic`, which enables the developer to take advantage of these classes while using completely custom logic to determine what initials need to be prompted for. :class:`CategoryLogic`, is a powerful :class:`HighScoreLogic` subclass that most developers will find suitable for implementing modern high score functionality. 13 | 14 | Finally, :func:`generate_highscore_frames` can help to quickly create a traditional high score display. 15 | 16 | Using EntrySequenceManager 17 | -------------------------- 18 | 19 | In your :class:`GameController` subclass's setup method, configure the categories you wish to track scores for. The categories are used each time the game ends, as we'll see in the next step. In this case we'll setup two categories: a 'classic' category for the traditional Grand Champion and high scores 1-4, and a more modern 'loop champ' category. We set :attr:`score_for_player` to tell the category how to obtain that particular score value. Note that because the loop champ only has one title, only the highest loop score will be saved. The number of titles is used to determine how many scores are saved. :: 20 | 21 | def setup(self): 22 | self.highscore_categories = [] 23 | 24 | cat = highscore.HighScoreCategory() 25 | cat.game_data_key = 'ClassicHighScoreData' 26 | cat.titles = ['Grand Champion', 'High Score 1', ... , `High Score 4`] 27 | self.highscore_categories.append(cat) 28 | 29 | cat = highscore.HighScoreCategory() 30 | cat.game_data_key = 'LoopsHighScoreData' 31 | cat.titles = ['Loop Champ'] 32 | cat.score_for_player = lambda category, player: player.loops 33 | cat.score_suffix_singular = ' loop' 34 | cat.score_suffix_plural = ' loops' 35 | self.highscore_categories.append(cat) 36 | 37 | for category in self.highscore_categories: 38 | category.load_from_game(self) 39 | 40 | We use :class:`EntrySequenceManager` to manage the high score prompting process once the game has finished. We instantiate it like a normal mode, set the finished handler and logic, and then add it to the mode queue:: 41 | 42 | def game_ended(self): 43 | seq_manager = highscore.EntrySequenceManager(game=self, priority=2) 44 | seq_manager.finished_handler = self.highscore_entry_finished 45 | seq_manager.logic = highscore.CategoryLogic(game=self, categories=self.highscore_categories) 46 | self.modes.add(seq_manager) 47 | 48 | Finally, we write the finished handler to remove the sequence manager and add the attract mode to prepare for starting a new game:: 49 | 50 | def highscore_entry_finished(self, mode): 51 | self.modes.remove(mode) 52 | self.start_attract_mode() 53 | 54 | An attract mode that displays the high scores might look like this:: 55 | 56 | class Attract(game.Mode): 57 | def mode_started(self): 58 | script = [{'seconds':2.0, 'layer':None}] 59 | for frame in highscore.generate_highscore_frames(self.game.highscore_categories): 60 | layer = dmd.FrameLayer(frame=frame) 61 | script.append({'seconds':2.0, 'layer':layer}) 62 | self.layer = dmd.ScriptedLayer(width=128, height=32, script=script) 63 | 64 | That the ``None`` layer allows the score display to be seen (as it is beneath the attract mode) for one period of the script. 65 | 66 | 67 | Default Scores 68 | -------------- 69 | 70 | Default scores and initials should be set using the game data template (the *template_filename* argument to :meth:`procgame.game.GameController.load_game_data`). The key must match the :attr:`HighScoreCategory.game_data_key` value. Example:: 71 | 72 | ClassicHighScores: 73 | - {inits: GSS, score: 5000000} 74 | - {inits: ASP, score: 4000000} 75 | - {inits: JRP, score: 3000000} 76 | - {inits: JAG, score: 2000000} 77 | - {inits: JTW, score: 1000000} 78 | LoopsHighScoreData: 79 | - {inits: GSS, score: 5} 80 | 81 | 82 | Classes 83 | ======= 84 | 85 | CategoryLogic 86 | ------------- 87 | 88 | .. autoclass:: procgame.highscore.CategoryLogic 89 | :members: 90 | 91 | EntryPrompt 92 | ----------- 93 | 94 | .. autoclass:: procgame.highscore.EntryPrompt 95 | :members: 96 | 97 | HighScore 98 | --------- 99 | 100 | .. autoclass:: procgame.highscore.HighScore 101 | :members: 102 | 103 | HighScoreCategory 104 | ----------------- 105 | 106 | .. autoclass:: procgame.highscore.HighScoreCategory 107 | :members: 108 | 109 | HighScoreLogic 110 | -------------- 111 | 112 | .. autoclass:: procgame.highscore.HighScoreLogic 113 | :members: 114 | 115 | InitialEntryMode 116 | ------------------ 117 | 118 | .. autoclass:: procgame.highscore.InitialEntryMode 119 | :members: 120 | 121 | EntrySequenceManager 122 | -------------------- 123 | 124 | .. autoclass:: procgame.highscore.EntrySequenceManager 125 | :members: 126 | 127 | Helper Methods 128 | ============== 129 | 130 | .. autofunction:: procgame.highscore.generate_highscore_frames -------------------------------------------------------------------------------- /procgame/desktop/desktop_pyglet.py: -------------------------------------------------------------------------------- 1 | import procgame.config 2 | import procgame.dmd 3 | import pinproc 4 | import pyglet 5 | import pyglet.image 6 | import pyglet.window 7 | from pyglet import gl 8 | 9 | # Bitmap data for luminance-alpha mask image. 10 | # See image_to_string below for code to generate this: 11 | MASK_DATA = """\x00\xec\x00\xc8\x00\x7f\x00\x5a\x00\x5a\x00\x7f\x00\xc8\x00\xed\x00\xc8\x00\x5a\x00\x36\x00\x11\x00\x11\x00\x36\x00\x5a\x00\xc8\x00\x7f\x00\x36\xff\x00\xff\x00\xff\x00\xff\x00\x00\x36\x00\x7e\x00\x5a\x00\x10\xff\x00\xff\x00\xff\x00\xff\x00\x00\x11\x00\x5a\x00\x5a\x00\x11\xff\x00\xff\x00\xff\x00\xff\x00\x00\x11\x00\x5a\x00\x7e\x00\x36\xff\x00\xff\x00\xff\x00\xff\x00\x00\x35\x00\x7f\x00\xc8\x00\x5a\x00\x36\x00\x11\x00\x11\x00\x35\x00\x5a\x00\xc8\x00\xed\x00\xc8\x00\x7e\x00\x5a\x00\x5a\x00\x7f\x00\xc8\x00\xed""" 12 | MASK_SIZE = 8 13 | 14 | DMD_SIZE = (128, 32) 15 | DMD_SCALE = int(procgame.config.value_for_key_path('desktop_dmd_scale', str(MASK_SIZE))) 16 | 17 | 18 | class Desktop(object): 19 | """The :class:`Desktop` class helps manage interaction with the desktop, providing both a windowed 20 | representation of the DMD, as well as translating keyboard input into pyprocgame events.""" 21 | 22 | exit_event_type = 99 23 | """Event type sent when Ctrl-C is received.""" 24 | 25 | key_map = {} 26 | 27 | window = None 28 | 29 | def __init__(self): 30 | self.key_events = [] 31 | self.setup_window() 32 | self.add_key_map(pyglet.window.key.LSHIFT, 3) 33 | self.add_key_map(pyglet.window.key.RSHIFT, 1) 34 | self.frame_drawer = FrameDrawer() 35 | 36 | def add_key_map(self, key, switch_number): 37 | """Maps the given *key* to *switch_number*, where *key* is one of the key constants in :mod:`pygame.locals`.""" 38 | self.key_map[key] = switch_number 39 | 40 | def clear_key_map(self): 41 | """Empties the key map.""" 42 | self.key_map = {} 43 | 44 | def get_keyboard_events(self): 45 | """Asks :mod:`pygame` for recent keyboard events and translates them into an array 46 | of events similar to what would be returned by :meth:`pinproc.PinPROC.get_events`.""" 47 | if self.window.has_exit: 48 | self.append_exit_event() 49 | e = self.key_events 50 | self.key_events = [] 51 | return e 52 | 53 | def append_exit_event(self): 54 | self.key_events.append({'type': self.exit_event_type, 'value': 'quit'}) 55 | 56 | def setup_window(self): 57 | self.window = pyglet.window.Window(width=DMD_SIZE[0] * DMD_SCALE, height=DMD_SIZE[1] * DMD_SCALE) 58 | 59 | @self.window.event 60 | def on_close(): 61 | self.append_exit_event() 62 | 63 | @self.window.event 64 | def on_key_press(symbol, modifiers): 65 | if (symbol == pyglet.window.key.C and modifiers & pyglet.window.key.MOD_CTRL) or ( 66 | symbol == pyglet.window.key.ESCAPE): 67 | self.append_exit_event() 68 | elif symbol in self.key_map: 69 | self.key_events.append({'type': pinproc.EventTypeSwitchClosedDebounced, 'value': self.key_map[symbol]}) 70 | 71 | @self.window.event 72 | def on_key_release(symbol, modifiers): 73 | if symbol in self.key_map: 74 | self.key_events.append({'type': pinproc.EventTypeSwitchOpenDebounced, 'value': self.key_map[symbol]}) 75 | 76 | def draw(self, frame): 77 | """Draw the given :class:`~procgame.dmd.Frame` in the window.""" 78 | self.window.dispatch_events() 79 | self.window.clear() 80 | self.frame_drawer.draw(frame) 81 | self.window.flip() 82 | 83 | def __str__(self): 84 | return '' 85 | 86 | 87 | class FrameDrawer(object): 88 | """Manages drawing a DMD frame using pyglet.""" 89 | 90 | def __init__(self): 91 | super(FrameDrawer, self).__init__() 92 | self.mask = pyglet.image.ImageData(MASK_SIZE, MASK_SIZE, 'LA', MASK_DATA, pitch=16) 93 | self.mask_texture = pyglet.image.TileableTexture.create_for_image(self.mask) 94 | 95 | def draw(self, frame): 96 | # The gneneral plan here is: 97 | # 1. Get the dots in the range of 0-255. 98 | # 2. Create a texture with the dots data. 99 | # 3. Draw the texture, scaled up with nearest-neighbor. 100 | # 4. Draw a mask over the dots to give them a slightly more realistic look. 101 | 102 | gl.glEnable(gl.GL_BLEND) 103 | gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 104 | gl.glLoadIdentity() 105 | 106 | # Draw the dots in this color: 107 | gl.glColor3f(1.0, 0.5, 0.25) 108 | 109 | gl.glScalef(1, -1, 1) 110 | gl.glTranslatef(0, -DMD_SIZE[1] * DMD_SCALE, 0) 111 | 112 | data = frame.get_data_mult() 113 | image = pyglet.image.ImageData(DMD_SIZE[0], DMD_SIZE[1], 'L', data, pitch=DMD_SIZE[0]) 114 | 115 | gl.glTexParameteri(image.get_texture().target, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST) 116 | image.blit(0, 0, width=DMD_SIZE[0] * DMD_SCALE, height=DMD_SIZE[1] * DMD_SCALE) 117 | 118 | del image 119 | 120 | gl.glScalef(DMD_SCALE / float(MASK_SIZE), DMD_SCALE / float(MASK_SIZE), 1.0) 121 | gl.glColor4f(1.0, 1.0, 1.0, 1.0) 122 | self.mask_texture.blit_tiled(x=0, y=0, z=0, width=DMD_SIZE[0] * MASK_SIZE, height=DMD_SIZE[1] * MASK_SIZE) 123 | 124 | 125 | def image_to_string(filename): 126 | """Generate a string representation of the image at the given path, for embedding in code.""" 127 | image = pyglet.image.load(filename) 128 | data = image.get_data('LA', 16) 129 | s = '' 130 | for x in data: 131 | s += "\\x%02x" % (ord(x)) 132 | return s 133 | -------------------------------------------------------------------------------- /procgame/desktop/desktop_pygame.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import procgame 3 | import pinproc 4 | from threading import Thread 5 | import random 6 | import string 7 | import time 8 | import locale 9 | import math 10 | import copy 11 | import ctypes 12 | from procgame.events import EventManager 13 | 14 | try: 15 | import pygame 16 | import pygame.locals 17 | except ImportError: 18 | print("Error importing pygame; ignoring.") 19 | pygame = None 20 | 21 | if hasattr(ctypes.pythonapi, 'Py_InitModule4'): 22 | Py_ssize_t = ctypes.c_int 23 | elif hasattr(ctypes.pythonapi, 'Py_InitModule4_64'): 24 | Py_ssize_t = ctypes.c_int64 25 | else: 26 | raise TypeError("Cannot determine type of Py_ssize_t") 27 | 28 | PyObject_AsWriteBuffer = ctypes.pythonapi.PyObject_AsWriteBuffer 29 | PyObject_AsWriteBuffer.restype = ctypes.c_int 30 | PyObject_AsWriteBuffer.argtypes = [ctypes.py_object, 31 | ctypes.POINTER(ctypes.c_void_p), 32 | ctypes.POINTER(Py_ssize_t)] 33 | 34 | 35 | def array(surface): 36 | buffer_interface = surface.get_buffer() 37 | address = ctypes.c_void_p() 38 | size = Py_ssize_t() 39 | PyObject_AsWriteBuffer(buffer_interface, 40 | ctypes.byref(address), ctypes.byref(size)) 41 | bytes = (ctypes.c_byte * size.value).from_address(address.value) 42 | bytes.object = buffer_interface 43 | return bytes 44 | 45 | 46 | class Desktop(): 47 | """The :class:`Desktop` class helps manage interaction with the desktop, providing both a windowed 48 | representation of the DMD, as well as translating keyboard input into pyprocgame events.""" 49 | 50 | exit_event_type = 99 51 | """Event type sent when Ctrl-C is received.""" 52 | 53 | key_map = {} 54 | 55 | def __init__(self): 56 | self.ctrl = 0 57 | self.i = 0 58 | 59 | if 'pygame' in globals(): 60 | self.setup_window() 61 | else: 62 | print('Desktop init skipping setup_window(); pygame does not appear to be loaded.') 63 | self.add_key_map(pygame.locals.K_LSHIFT, 3) 64 | self.add_key_map(pygame.locals.K_RSHIFT, 1) 65 | 66 | def add_key_map(self, key, switch_number): 67 | """Maps the given *key* to *switch_number*, where *key* is one of the key constants in :mod:`pygame.locals`.""" 68 | self.key_map[key] = switch_number 69 | 70 | def clear_key_map(self): 71 | """Empties the key map.""" 72 | self.key_map = {} 73 | 74 | def get_keyboard_events(self): 75 | """Asks :mod:`pygame` for recent keyboard events and translates them into an array 76 | of events similar to what would be returned by :meth:`pinproc.PinPROC.get_events`.""" 77 | key_events = [] 78 | for event in pygame.event.get(): 79 | EventManager.default().post(name=self.event_name_for_pygame_event_type(event.type), object=self, info=event) 80 | key_event = {} 81 | if event.type == pygame.locals.KEYDOWN: 82 | if event.key == pygame.locals.K_RCTRL or event.key == pygame.locals.K_LCTRL: 83 | self.ctrl = 1 84 | if event.key == pygame.locals.K_c: 85 | if self.ctrl == 1: 86 | key_event['type'] = self.exit_event_type 87 | key_event['value'] = 'quit' 88 | elif event.key == pygame.locals.K_ESCAPE: 89 | key_event['type'] = self.exit_event_type 90 | key_event['value'] = 'quit' 91 | elif event.key in self.key_map: 92 | key_event['type'] = pinproc.EventTypeSwitchClosedDebounced 93 | key_event['value'] = self.key_map[event.key] 94 | elif event.type == pygame.locals.KEYUP: 95 | if event.key == pygame.locals.K_RCTRL or event.key == pygame.locals.K_LCTRL: 96 | self.ctrl = 0 97 | elif event.key in self.key_map: 98 | key_event['type'] = pinproc.EventTypeSwitchOpenDebounced 99 | key_event['value'] = self.key_map[event.key] 100 | if len(key_event): 101 | key_events.append(key_event) 102 | return key_events 103 | 104 | event_listeners = {} 105 | 106 | def event_name_for_pygame_event_type(self, event_type): 107 | return 'pygame(%s)' % event_type 108 | 109 | screen = None 110 | """:class:`pygame.Surface` object representing the screen's surface.""" 111 | screen_multiplier = 4 112 | 113 | def setup_window(self): 114 | pygame.init() 115 | self.screen = pygame.display.set_mode((128 * self.screen_multiplier, 32 * self.screen_multiplier)) 116 | pygame.display.set_caption('Press CTRL-C to exit') 117 | 118 | def draw(self, frame): 119 | """Draw the given :class:`~procgame.dmd.Frame` in the window.""" 120 | # Use adjustment to add a one pixel border around each dot, if 121 | # the screen size is large enough to accomodate it. 122 | if self.screen_multiplier >= 4: 123 | adjustment = -1 124 | else: 125 | adjustment = 0 126 | 127 | bytes_per_pixel = 4 128 | y_offset = 128 * bytes_per_pixel * self.screen_multiplier * self.screen_multiplier 129 | x_offset = bytes_per_pixel * self.screen_multiplier 130 | 131 | surface_array = array(self.screen) 132 | 133 | frame_string = frame.get_data() 134 | 135 | x = 0 136 | y = 0 137 | for dot in frame_string: 138 | color_val = ord(dot) * 16 139 | index = y * y_offset + x * x_offset 140 | surface_array[index:index + bytes_per_pixel] = (color_val, color_val, color_val, 0) 141 | x += 1 142 | if x == 128: 143 | x = 0 144 | y += 1 145 | del surface_array 146 | 147 | pygame.display.update() 148 | 149 | def __str__(self): 150 | return '' 151 | -------------------------------------------------------------------------------- /docs/sphinx/ref/dmd.rst: -------------------------------------------------------------------------------- 1 | ************* 2 | dmd Submodule 3 | ************* 4 | 5 | .. module:: procgame.dmd 6 | 7 | The dmd submodule provides classes and functions to facilitate generating both text and graphics for on the dot matrix display: 8 | 9 | * Load and save :file:`.dmd` files from/to disk using :meth:`Animation.load` and :meth:`~Animation.save`. 10 | * Extract and reorder individual :class:`Frame` objects that compose an :class:`Animation` with :attr:`Animation.frames`. 11 | * Use :class:`Layer` subclasses such as :class:`GroupedLayer` to control and generate sophisticated sequences. 12 | * Display a sequence of frames using :class:`AnimatedLayer`. 13 | * Automatically update the DMD based on the active modes using :class:`DisplayController`. 14 | 15 | Overview 16 | ======== 17 | 18 | The pyprocgame display system is designed specifically to take advantage of the mode queue architecture. This provides the game developer with a relatively simple means to supply the player with the most relevant information at all times. 19 | 20 | The :class:`DisplayController` class is the glue that makes this architecture work. If you wish to implement a different architecture you can certainly ignore :class:`DisplayController` but most developers will want to take advantage of it. 21 | 22 | When integrated within your :class:`~procgame.game.GameController` subclass, :class:`DisplayController`'s :meth:`~DisplayController.update` method is called whenever the DMD is ready for a new frame. :meth:`update` iterates over the mode queue in search of modes that have a :attr:`layer` attribute. This attribute should be an instance of a subclass of :class:`Layer`, which provides a sequence of frames, one at a time, via its :meth:`~Layer.next_frame` method. If a mode does not provide this layer attribute it is ignored; otherwise the next frame is obtained and the frames are composited from bottom to top (lowest priority modes being at the bottom). 23 | 24 | All layers have an :attr:`Layer.opaque` attribute, which defaults to ``False``. If a layer is opaque, no layers below that one (with lower priority) will be fetched or composited. Also, if a layer's :meth:`next_frame` method returns ``None`` it is considered to be transparent. 25 | 26 | By using this mode-layer arrangement the developer has a direct connection between modes and what's being displayed, without having to manage a separate display queue. For example, the score display might be a layer associated with a low-priority mode. If the game enters a hurry-up mode, which has a higher priority due to its interest in switch events, the hurry-up mode can supply its own layer which is automatically displayed above the score layer. 27 | 28 | .. 29 | TODO: Talk about transitions and their hook-in in layers. 30 | 31 | 32 | Core Classes 33 | ============ 34 | 35 | Animation 36 | --------- 37 | .. autoclass:: procgame.dmd.Animation 38 | :members: 39 | 40 | DisplayController 41 | ----------------- 42 | .. autoclass:: procgame.dmd.DisplayController 43 | :members: 44 | 45 | Font 46 | ----- 47 | .. autoclass:: procgame.dmd.Font 48 | :members: 49 | 50 | Frame 51 | ----- 52 | .. autoclass:: procgame.dmd.Frame 53 | :members: 54 | 55 | Layer 56 | ----- 57 | .. autoclass:: procgame.dmd.Layer 58 | :members: 59 | 60 | 61 | 62 | Layer Subclasses 63 | ================ 64 | 65 | Subclasses of :class:`Layer` that provide building blocks for sophisticated display effects. 66 | 67 | AnimatedLayer 68 | ------------- 69 | .. autoclass:: procgame.dmd.AnimatedLayer 70 | :members: 71 | 72 | FrameLayer 73 | ---------- 74 | .. autoclass:: procgame.dmd.FrameLayer 75 | :members: 76 | 77 | GroupedLayer 78 | ------------ 79 | .. autoclass:: procgame.dmd.GroupedLayer 80 | :members: 81 | 82 | 83 | ScriptedLayer 84 | ------------- 85 | .. autoclass:: procgame.dmd.ScriptedLayer 86 | :members: 87 | 88 | TextLayer 89 | --------- 90 | .. autoclass:: procgame.dmd.TextLayer 91 | :members: 92 | 93 | 94 | .. 95 | Undocumented for now: 96 | 97 | Layer Transitions 98 | ================= 99 | 100 | LayerTransitionBase 101 | ------------------- 102 | .. autoclass:: procgame.dmd.LayerTransitionBase 103 | :members: 104 | 105 | CrossFadeTransition 106 | ------------------- 107 | .. autoclass:: procgame.dmd.CrossFadeTransition 108 | :members: 109 | 110 | ExpandTransition 111 | ---------------- 112 | .. autoclass:: procgame.dmd.ExpandTransition 113 | :members: 114 | 115 | PushLayerTransition 116 | ------------------- 117 | .. autoclass:: procgame.dmd.PushLayerTransition 118 | :members: 119 | 120 | SlideOverLayerTransition 121 | ------------------------ 122 | .. autoclass:: procgame.dmd.SlideOverLayerTransition 123 | :members: 124 | 125 | 126 | 127 | Utilities 128 | ========= 129 | 130 | Font Utilities 131 | -------------- 132 | 133 | .. autofunction:: procgame.dmd.font_named 134 | .. autodata:: procgame.dmd.font_path 135 | 136 | MarkupFrameGenerator 137 | -------------------- 138 | 139 | .. autoclass:: procgame.dmd.MarkupFrameGenerator 140 | :members: 141 | 142 | .. 143 | Undocumented for now: 144 | 145 | TransitionOutHelperMode 146 | ----------------------- 147 | 148 | .. autoclass:: procgame.dmd.TransitionOutHelperMode 149 | :members: 150 | 151 | 152 | .. _dmd-format: 153 | 154 | DMD File Format Specification 155 | ============================= 156 | 157 | .dmd files are the native format for DMD animations in pyprocgame. 158 | The file format is as follows:: 159 | 160 | 4 bytes - header data (unused) 161 | 4 bytes - frame_count 162 | 4 bytes - width of animation frames in pixels 163 | 4 bytes - height of animation frames in pixels 164 | ? bytes - Frames: frame_count * width * height bytes 165 | 166 | Frame data is laid out row0..rowN. Byte values of each pixel 167 | are in two parts: the lower 4 bits are the dot "color", ``0x0`` 168 | being black and ``0xF`` being the brightest value and the upper 169 | 4 bits are alpha (``0x0`` is fully transparent, ``0xF`` is fully 170 | opaque). Note that transparency is optional and only supported 171 | by the alpha blending modes in :meth:`procgame.dmd.Frame.copy_rect`. 172 | Alpha values are ignored by :meth:`pinproc.PinPROC.dmd_draw`. 173 | 174 | .dmd files are loaded using :meth:`procgame.dmd.Animation.load`. 175 | -------------------------------------------------------------------------------- /procgame/highscore/sequence.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .entry import * 4 | from .. import game 5 | 6 | 7 | class EntryPrompt: 8 | """Used by :class:`HighScoreLogic` subclasses' :meth:`HighScoreLogic.prompts` methods 9 | to communicate which scores need to be prompted for. 10 | """ 11 | 12 | key = None 13 | """Object that will be used to identify this prompt when :meth:`HighScoreLogic.store_initials` is called.""" 14 | 15 | left = None 16 | """String or array of strings to be displayed on the left side of :class:`InitialEntryMode`.""" 17 | 18 | right = None 19 | """String or array of strings to be displayed on the right side of :class:`InitialEntryMode`.""" 20 | 21 | def __init__(self, key=None, left=None, right=None): 22 | self.key = key 23 | self.left = left 24 | self.right = right 25 | 26 | 27 | class HighScoreLogic: 28 | """Interface used by :class:`EntrySequenceManager` to abstract away the details of high score entry and storage.""" 29 | 30 | def prompts(self): 31 | """Return a list of :class:`EntryPrompt` objects to be presented to the player, in order. 32 | """ 33 | return list() 34 | 35 | def store_initials(self, key, inits): 36 | """Called by :class:`EntrySequenceManager` to store the entered initials.""" 37 | pass 38 | 39 | 40 | class HighScore: 41 | """Model class. 42 | 43 | :attr:`score`, :attr:`inits` and :attr:`date` are persisted via the dictionary with 44 | :meth:`from_dict` and :meth:`to_dict`. The remaining attributes are used to maintain state. 45 | """ 46 | 47 | score = 0 48 | """Numeric high score value.""" 49 | 50 | inits = None 51 | """Player's initials.""" 52 | 53 | date = None 54 | """String date representation of this score, using :func:`time.asctime`.""" 55 | 56 | key = None 57 | """Object value used to uniquely identify this score.""" 58 | 59 | name = None 60 | """Player's name, such as `Player 1`.""" 61 | 62 | title = None 63 | """Title for this score, such as `Grand Champion`.""" 64 | 65 | def __init__(self, score=None, inits=None, name=None, key=None): 66 | self.score = score 67 | self.inits = inits 68 | self.name = name 69 | self.key = key 70 | self.date = time.asctime() 71 | 72 | def __repr__(self): 73 | return '<%s score=%d inits=%s>' % (self.__class__.__name__, self.score, self.inits) 74 | 75 | def from_dict(self, src): 76 | """Populate the high score value from a dictionary. 77 | Requires `score` and `inits` keys, may include `date`.""" 78 | self.score = src['score'] 79 | self.inits = src['inits'] 80 | if 'date' in src: 81 | self.date = src['date'] 82 | return self 83 | 84 | def to_dict(self): 85 | """Returns a dictionary representation of this high score, 86 | including `score`, `inits`, and `date`.""" 87 | return {'score': self.score, 'inits': self.inits, 'date': self.date} 88 | 89 | def __cmp__(self, other): 90 | c = (self.score > other.score) - (self.score < other.score) 91 | if c == 0: 92 | return (other.date > self.date) - (other.date < self.date) 93 | else: 94 | return c 95 | 96 | 97 | class EntrySequenceManager(game.Mode): 98 | """A :class:`~procgame.game.Mode` subclass that manages the presentation of :class:`InitialEntryMode` 99 | in order to prompt the player(s) for new high scores. 100 | 101 | The :attr:`logic` attribute should be set to an instance of :class:`HighScoreLogic`, 102 | which is used to customize the behavior of the sequence manager. The behavior of 103 | this class can be modified by supplying different subclasses of :class:`HighScoreLogic`. 104 | 105 | This mode does not remove itself from the mode queue. 106 | Set :attr:`finished_handler` to a method to call once the sequence is completed. 107 | The handler will be called immediately (once this mode is added to the mode queue) 108 | if there are no high scores to be entered. 109 | """ 110 | 111 | prompts = None 112 | active_prompt = None 113 | 114 | logic = None 115 | """Set this attribute to an instance of :class:`HighScoreLogic`.""" 116 | 117 | ready_handler = None 118 | """Method taking two objects: this class instance and the :class:`EntryPrompt` to be shown next. 119 | The implementor must call :meth:`prompt` in order to present the initials entry mode, otherwise 120 | the sequence will not proceed. If this attribute is not set then initials entry mode will be 121 | shown immediately. 122 | This allows for special displays or interaction before each initials prompt. 123 | """ 124 | 125 | finished_handler = None 126 | """Method taking one parameter, the mode (this object instance).""" 127 | 128 | def mode_started(self): 129 | self.prompts = self.logic.prompts() 130 | self.next() 131 | 132 | def next(self): 133 | if len(self.prompts) > 0: 134 | self.active_prompt = self.prompts[0] 135 | del self.prompts[0] 136 | if self.ready_handler: 137 | self.ready_handler(self, self.active_prompt) 138 | else: 139 | self.prompt() 140 | else: 141 | if self.finished_handler is not None: 142 | self.finished_handler(mode=self) 143 | 144 | def prompt(self): 145 | """To be called externally if using the :attr:`ready_handler`, once that handler has been called. 146 | Presents the initials entry mode.""" 147 | self.prompt_for_initials(left_text=self.active_prompt.left, right_text=self.active_prompt.right) 148 | 149 | def create_highscore_entry_mode(self, left_text, right_text, entered_handler): 150 | """Subclasses can override this to supply their own entry handler.""" 151 | return InitialEntryMode(game=self.game, priority=self.priority + 1, left_text=left_text, right_text=right_text, 152 | entered_handler=entered_handler) 153 | 154 | def prompt_for_initials(self, left_text, right_text): 155 | self.highscore_entry = self.create_highscore_entry_mode(left_text, right_text, self.highscore_entered) 156 | self.add_child_mode(self.highscore_entry) 157 | 158 | def highscore_entered(self, mode, inits): 159 | self.logic.store_initials(key=self.active_prompt.key, inits=inits) 160 | self.remove_child_mode(self.highscore_entry) # same as *mode* 161 | self.next() 162 | -------------------------------------------------------------------------------- /tools/highscoretest.py: -------------------------------------------------------------------------------- 1 | import locale 2 | import sys 3 | 4 | sys.path.append(sys.path[ 5 | 0] + '/..') # Set the path so we can find procgame. We are assuming (stupidly?) that the first member is our directory. 6 | from procgame import * 7 | 8 | locale.setlocale(locale.LC_ALL, "") # Used to put commas in the score. 9 | 10 | config_path = "JD.yaml" 11 | 12 | 13 | class BallEnder(game.Mode): 14 | """The exit switch is used to end each ball.""" 15 | 16 | def sw_exit_active(self, sw): 17 | self.game.current_player().score += 350000 18 | self.game.end_ball() 19 | 20 | 21 | class BaseGameMode(game.Mode): 22 | """A minimal game mode to enable starting a game.""" 23 | 24 | def sw_startButton_active(self, sw): 25 | if self.game.ball == 0: 26 | self.game.start_game() 27 | self.game.add_player() 28 | self.game.start_ball() 29 | elif self.game.ball == 1: 30 | p = self.game.add_player() 31 | self.game.set_status(p.name + " added!") 32 | else: 33 | self.game.set_status("Hold for 2s to reset.") 34 | 35 | def sw_startButton_active_for_2s(self, sw): 36 | if self.game.ball > 1: 37 | self.game.set_status("Reset!") 38 | self.game.reset() 39 | return True 40 | 41 | 42 | class Attract(game.Mode): 43 | def mode_started(self): 44 | # Create a ScriptedLayer with frames for each of the high scores: 45 | script = [] 46 | 47 | # Cheating a bit here to make the score display have a transition, since it it always on: 48 | script.append({'seconds': 3.0, 'layer': self.game.score_display.layer}) 49 | self.game.score_display.layer.transition = dmd.PushTransition(direction='south') 50 | 51 | for frame in highscore.generate_highscore_frames(self.game.highscore_categories): 52 | layer = dmd.FrameLayer(frame=frame) 53 | layer.transition = dmd.PushTransition(direction='south') 54 | script.append({'seconds': 2.0, 'layer': layer}) 55 | 56 | self.layer = dmd.ScriptedLayer(width=128, height=32, script=script) 57 | 58 | # Opaque allows transitions between scripted layer 'frames' to work: 59 | self.layer.opaque = True 60 | 61 | 62 | class TestGame(game.BasicGame): 63 | highscore_categories = None 64 | 65 | def __init__(self, machine_type): 66 | super(TestGame, self).__init__(machine_type) 67 | 68 | def setup(self): 69 | self.load_config(config_path) 70 | self.desktop.add_key_map(ord('q'), self.switches.exit.number) 71 | 72 | self.highscore_categories = [] 73 | 74 | cat = highscore.HighScoreCategory() 75 | # because we don't have a game_data template: 76 | cat.scores = [highscore.HighScore(score=5000000, inits='GSS'), \ 77 | highscore.HighScore(score=4000000, inits='ASP'), \ 78 | highscore.HighScore(score=3000000, inits='JRP'), \ 79 | highscore.HighScore(score=2000000, inits='JAG'), \ 80 | highscore.HighScore(score=1000000, inits='JTW')] 81 | cat.game_data_key = 'ClassicHighScoreData' 82 | self.highscore_categories.append(cat) 83 | 84 | cat = highscore.HighScoreCategory() 85 | cat.game_data_key = 'LoopsHighScoreData' 86 | # because we don't have a game_data template: 87 | cat.scores = [highscore.HighScore(score=5, inits='GSS')] 88 | cat.titles = ['Loop Champ'] 89 | cat.score_suffix_singular = ' loop' 90 | cat.score_suffix_plural = ' loops' 91 | self.highscore_categories.append(cat) 92 | 93 | for category in self.highscore_categories: 94 | category.load_from_game(self) 95 | 96 | self.reset() 97 | 98 | def reset(self): 99 | super(TestGame, self).reset() 100 | self.modes.add(BaseGameMode(game=self, priority=1)) 101 | self.modes.add(BallEnder(game=self, priority=1)) 102 | # Make sure flippers are off, especially for user initiated resets. 103 | self.enable_flippers(enable=False) 104 | self.add_attract() 105 | 106 | def game_started(self): 107 | self.modes.remove(self.attract) 108 | self.attract = None 109 | 110 | def game_ended(self): 111 | seq_manager = highscore.EntrySequenceManager(game=self, priority=2) 112 | seq_manager.ready_handler = self.highscore_entry_ready_to_prompt 113 | seq_manager.finished_handler = self.highscore_entry_finished 114 | 115 | seq_manager.logic = highscore.CategoryLogic(game=self, categories=self.highscore_categories) 116 | self.modes.add(seq_manager) 117 | 118 | def add_attract(self): 119 | self.attract = Attract(game=self, priority=8) 120 | self.modes.add(self.attract) 121 | 122 | def highscore_entry_ready_to_prompt(self, mode, prompt): 123 | banner_mode = game.Mode(game=self, priority=8) 124 | markup = dmd.MarkupFrameGenerator() 125 | markup.font_plain = dmd.font_named('Font09Bx7.dmd') 126 | markup.font_bold = dmd.font_named('Font13Bx9.dmd') 127 | text = '[Great Score]\n#%s#' % (prompt.left.upper()) # we know that the left is the player name 128 | frame = markup.frame_for_markup(markup=text, y_offset=0) 129 | frame_layer = dmd.FrameLayer(frame=frame) 130 | frame_layer.blink_frames = 10 131 | banner_mode.layer = dmd.ScriptedLayer(width=128, height=32, script=[{'seconds': 3.0, 'layer': frame_layer}]) 132 | banner_mode.layer.on_complete = lambda: self.highscore_banner_complete(banner_mode=banner_mode, 133 | highscore_entry_mode=mode) 134 | self.modes.add(banner_mode) 135 | 136 | def highscore_banner_complete(self, banner_mode, highscore_entry_mode): 137 | self.modes.remove(banner_mode) 138 | highscore_entry_mode.prompt() 139 | 140 | def highscore_entry_finished(self, mode): 141 | self.modes.remove(mode) 142 | self.add_attract() 143 | 144 | def set_status(self, text): 145 | self.dmd.set_message(text, 3) 146 | print(text) 147 | 148 | 149 | def main(): 150 | config = game.config_named(config_path) 151 | machine_type = config['PRGame']['machineType'] 152 | del config 153 | test_game = None 154 | try: 155 | test_game = TestGame(machine_type) 156 | test_game.setup() 157 | test_game.run_loop() 158 | finally: 159 | del test_game 160 | 161 | 162 | if __name__ == '__main__': main() 163 | -------------------------------------------------------------------------------- /procgame/dmd/dmd.py: -------------------------------------------------------------------------------- 1 | import pinproc 2 | 3 | 4 | class Frame(pinproc.DMDBuffer): 5 | """DMD frame/bitmap. 6 | 7 | Subclass of :class:`pinproc.DMDBuffer`. 8 | """ 9 | 10 | width = 0 11 | """Width of the frame in dots.""" 12 | height = 0 13 | """Height of the frame in dots.""" 14 | 15 | def __init__(self, width, height): 16 | """Initializes the frame to the given `width` and `height`.""" 17 | super(Frame, self).__init__(width, height) 18 | self.width = width 19 | self.height = height 20 | 21 | def copy_rect(dst, dst_x, dst_y, src, src_x, src_y, width, height, op="copy"): 22 | """Static method which performs some type checking before calling :meth:`pinproc.DMDBuffer.copy_to_rect`.""" 23 | if not (issubclass(type(dst), pinproc.DMDBuffer) and issubclass(type(src), pinproc.DMDBuffer)): 24 | raise ValueError("Incorrect types") 25 | src.copy_to_rect(dst, int(dst_x), int(dst_y), int(src_x), int(src_y), int(width), int(height), op) 26 | 27 | copy_rect = staticmethod(copy_rect) 28 | 29 | def subframe(self, x, y, width, height): 30 | """Generates a new frame based on a sub rectangle of this frame.""" 31 | subframe = Frame(width, height) 32 | Frame.copy_rect(subframe, 0, 0, self, x, y, width, height, 'copy') 33 | return subframe 34 | 35 | def copy(self): 36 | """Returns a copy of itself.""" 37 | frame = Frame(self.width, self.height) 38 | frame.set_data(self.get_data()) 39 | return frame 40 | 41 | def ascii(self): 42 | """Returns an ASCII representation of itself.""" 43 | output = '' 44 | table = [' ', '.', '.', '.', ',', ',', ',', '-', '-', '=', '=', '=', '*', '*', '#', '#', ] 45 | for y in range(self.height): 46 | for x in range(self.width): 47 | dot = self.get_dot(x, y) 48 | output += table[dot & 0xf] 49 | output += "\n" 50 | return output 51 | 52 | def create_with_text(lines, palette={' ': 0, '*': 15}): 53 | """Create a frame based on text. 54 | 55 | This class method can be used to generate small sprites within the game's source code:: 56 | 57 | frame = Frame.create_with_text(lines=[ \\ 58 | '*+++*', \\ 59 | ' *+* ', \\ 60 | ' * '], palette={' ':0, '+':7, '*':15}) 61 | """ 62 | height = len(lines) 63 | if height > 0: 64 | width = len(lines[0]) 65 | else: 66 | width = 0 67 | frame = Frame(width, height) 68 | for y in range(height): 69 | for x in range(width): 70 | char = lines[y][x] 71 | frame.set_dot(x, y, palette[char]) 72 | return frame 73 | 74 | create_with_text = staticmethod(create_with_text) 75 | 76 | def create_frames_from_grid(self, num_cols, num_rows): 77 | frames = [] 78 | width = self.width / num_cols 79 | height = self.height / num_rows 80 | 81 | # Use nested loops to step through each column of each row, creating a new frame at each iteration and copying in the appropriate data. 82 | for row_index in range(0, num_rows): 83 | for col_index in range(0, num_cols): 84 | new_frame = Frame(width, height) 85 | Frame.copy_rect(dst=new_frame, dst_x=0, dst_y=0, src=self, src_x=width * col_index, 86 | src_y=height * row_index, width=width, height=height, op='copy') 87 | frames += [new_frame] 88 | return frames 89 | 90 | 91 | class Layer(object): 92 | """ 93 | The ``Layer`` class is the basis for the pyprocgame display architecture. 94 | Subclasses override :meth:`next_frame` to provide a frame for the current moment in time. 95 | Handles compositing of provided frames and applying transitions within a :class:`DisplayController` context. 96 | """ 97 | 98 | opaque = False 99 | """Determines whether layers below this one will be rendered. 100 | If `True`, the :class:`DisplayController` will not render any layers after this one 101 | (such as from modes with lower priorities -- see :class:`DisplayController` for more information). 102 | """ 103 | 104 | target_x = 0 105 | """Base `x` component of the coordinates at which this layer will be composited upon a target buffer.""" 106 | target_y = 0 107 | """Base `y` component of the coordinates at which this layer will be composited upon a target buffer.""" 108 | target_x_offset = 0 109 | """Translation component used in addition to :attr:`target_x` as this layer's final compositing position.""" 110 | target_y_offset = 0 111 | """Translation component used in addition to :attr:`target_y` as this layer's final compositing position.""" 112 | enabled = True 113 | """If `False`, :class:`DisplayController` will ignore this layer.""" 114 | composite_op = 'copy' 115 | """Composite operation used by :meth:`composite_next` when calling :meth:`~pinproc.DMDBuffer.copy_rect`.""" 116 | transition = None 117 | """Transition which :meth:`composite_next` applies to the result of :meth:`next_frame` prior to compositing upon the output.""" 118 | 119 | def __init__(self, opaque=False): 120 | """Initialize a new Layer object.""" 121 | super(Layer, self).__init__() 122 | self.opaque = opaque 123 | self.set_target_position(0, 0) 124 | 125 | def reset(self): 126 | # To be overridden 127 | pass 128 | 129 | def set_target_position(self, x, y): 130 | """Setter for :attr:`target_x` and :attr:`target_y`.""" 131 | self.target_x = x 132 | self.target_y = y 133 | 134 | def next_frame(self): 135 | """Returns an instance of a Frame object to be shown, or None if there is no frame. 136 | The default implementation returns ``None``; subclasses should implement this method.""" 137 | return None 138 | 139 | def composite_next(self, target): 140 | """Composites the next frame of this layer onto the given target buffer. 141 | Called by :meth:`DisplayController.update`. 142 | Generally subclasses should not override this method; implementing :meth:`next_frame` is recommended instead. 143 | """ 144 | src = self.next_frame() 145 | if src is not None: 146 | if self.transition is not None: 147 | src = self.transition.next_frame(from_frame=target, to_frame=src) 148 | Frame.copy_rect(dst=target, dst_x=self.target_x + self.target_x_offset, 149 | dst_y=self.target_y + self.target_y_offset, src=src, src_x=0, src_y=0, width=src.width, 150 | height=src.height, op=self.composite_op) 151 | return src 152 | -------------------------------------------------------------------------------- /procgame/alphanumeric.py: -------------------------------------------------------------------------------- 1 | import pinproc 2 | 3 | 4 | class AlphanumericDisplay(object): 5 | # Start at ASCII table offset 32: ' ' 6 | asciiSegments = [0x0000, # ' ' 7 | 0x0000, # '!' 8 | 0x0000, # '"' 9 | 0x0000, # '#' 10 | 0x0000, # '$' 11 | 0x0000, # '%' 12 | 0x0000, # '&' 13 | 0x0200, # ''' 14 | 0x1400, # '(' 15 | 0x4100, # ')' 16 | 0x7f40, # '*' 17 | 0x2a40, # '+' 18 | 0x8080, # ',' 19 | 0x0840, # '-' 20 | 0x8000, # '.' 21 | 0x4400, # '/' 22 | 23 | 0x003f, # '0' 24 | 0x0006, # '1' 25 | 0x085b, # '2' 26 | 0x084f, # '3' 27 | 0x0866, # '4' 28 | 0x086d, # '5' 29 | 0x087d, # '6' 30 | 0x0007, # '7' 31 | 0x087f, # '8' 32 | 0x086f, # '9' 33 | 34 | 0x0000, # '1' 35 | 0x0000, # '1' 36 | 0x0000, # '1' 37 | 0x0000, # '1' 38 | 0x0000, # '1' 39 | 0x0000, # '1' 40 | 0x0000, # '1' 41 | 42 | 0x0877, # 'A' 43 | 0x2a4f, # 'B' 44 | 0x0039, # 'C' 45 | 0x220f, # 'D' 46 | 0x0879, # 'E' 47 | 0x0871, # 'F' 48 | 0x083d, # 'G' 49 | 0x0876, # 'H' 50 | 0x2209, # 'I' 51 | 0x001e, # 'J' 52 | 0x1470, # 'K' 53 | 0x0038, # 'L' 54 | 0x0536, # 'M' 55 | 0x1136, # 'N' 56 | 0x003f, # 'O' 57 | 0x0873, # 'P' 58 | 0x103f, # 'Q' 59 | 0x1873, # 'R' 60 | 0x086d, # 'S' 61 | 0x2201, # 'T' 62 | 0x003e, # 'U' 63 | 0x4430, # 'V' 64 | 0x5036, # 'W' 65 | 0x5500, # 'X' 66 | 0x2500, # 'Y' 67 | 0x4409 # 'Z' 68 | ] 69 | 70 | strobes = [8, 9, 10, 11, 12] 71 | full_intensity_delay = 350 # microseconds 72 | inter_char_delay = 40 # microseconds 73 | 74 | def __init__(self, aux_controller): 75 | """Initializes the animation.""" 76 | super(AlphanumericDisplay, self).__init__() 77 | 78 | self.aux_controller = aux_controller 79 | self.aux_index = aux_controller.get_index() 80 | 81 | def display(self, input_strings, intensities=[[1] * 16] * 2): 82 | 83 | strings = [] 84 | 85 | # Make sure strings are at least 16 chars. 86 | # Then convert each string to a list of chars. 87 | for j in range(0, 2): 88 | input_strings[j] = input_strings[j].upper() 89 | if len(input_strings[j]) < 16: input_strings[j] += ' ' * (16 - len(input_strings[j])) 90 | strings += [list(input_strings[j])] 91 | 92 | # Make sure insensities are 1 or less 93 | for i in range(0, 16): 94 | for j in range(0, 2): 95 | if intensities[j][i] > 1: intensities[j][i] = 1 96 | 97 | commands = [] 98 | segs = [] 99 | char_on_time = [] 100 | char_off_time = [] 101 | 102 | # Initialize a 2x16 array for segments value 103 | segs = [[0] * 16 for i in xrange(2)] 104 | 105 | # Loop through each character 106 | for i in range(0, 16): 107 | 108 | # Activate the character position (this goes to both displayas) 109 | commands += [pinproc.aux_command_output_custom(i, 0, self.strobes[0], False, 0)] 110 | 111 | for j in range(0, 2): 112 | segs[j][i] = self.asciiSegments[ord(strings[j][i]) - 32] 113 | 114 | # Check for commas or periods. 115 | # If found, squeeze comma into previous character. 116 | # No point checking the last character (plus, this avoids an 117 | # indexing error by not checking i+1 on the 16th char. 118 | if i < 15: 119 | comma_dot = strings[j][i + 1] 120 | if comma_dot == "," or comma_dot == ".": 121 | segs[j][i] |= self.asciiSegments[ord(comma_dot) - 32] 122 | strings[j].remove(comma_dot) 123 | # Append a space to ensure there are enough chars. 124 | strings[j].append(' ') 125 | 126 | commands += [pinproc.aux_command_output_custom(segs[j][i] & 0xff, 0, self.strobes[j * 2 + 1], False, 0)] 127 | commands += [ 128 | pinproc.aux_command_output_custom((segs[j][i] >> 8) & 0xff, 0, self.strobes[j * 2 + 2], False, 0)] 129 | char_on_time += [intensities[j][i] * self.full_intensity_delay] 130 | char_off_time += [self.inter_char_delay + (self.full_intensity_delay - char_on_time[j])] 131 | 132 | if char_on_time[0] < char_on_time[1]: 133 | first = 0 134 | second = 1 135 | else: 136 | first = 1 137 | second = 0 138 | 139 | # Determine amount of time to leave the other char on after the 140 | # first is off. 141 | between_delay = char_on_time[second] - char_on_time[first] 142 | 143 | # Not sure if the hardware will like a delay of 0 144 | # Use 2 to be extra safe. 2 microseconds won't affect display. 145 | if between_delay == 0: between_delay = 2 146 | 147 | # Delay until it's time to turn off the character with the lowest intensity 148 | commands += [pinproc.aux_command_delay(char_on_time[first])] 149 | commands += [pinproc.aux_command_output_custom(0, 0, self.strobes[first * 2 + 1], False, 0)] 150 | commands += [pinproc.aux_command_output_custom(0, 0, self.strobes[first * 2 + 2], False, 0)] 151 | 152 | # Delay until it's time to turn off the other character. 153 | commands += [pinproc.aux_command_delay(between_delay)] 154 | commands += [pinproc.aux_command_output_custom(0, 0, self.strobes[second * 2 + 1], False, 0)] 155 | commands += [pinproc.aux_command_output_custom(0, 0, self.strobes[second * 2 + 2], False, 0)] 156 | 157 | # Delay for the inter-digit delay. 158 | commands += [pinproc.aux_command_delay(char_off_time[second])] 159 | 160 | # Send the new list of commands to the Aux port controller. 161 | self.aux_controller.update(self.aux_index, commands) 162 | 163 | for command in commands: 164 | print("Aux Command: %s" % command) 165 | -------------------------------------------------------------------------------- /docs/sphinx/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pyprocgame documentation build configuration file, created by 4 | # sphinx-quickstart on Fri May 14 22:35:29 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | import time 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.append(os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be extensions 25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 26 | extensions = ['sphinx.ext.autodoc'] 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ['_templates'] 30 | 31 | # The suffix of source filenames. 32 | source_suffix = '.rst' 33 | 34 | # The encoding of source files. 35 | #source_encoding = 'utf-8' 36 | 37 | # The master toctree document. 38 | master_doc = 'index' 39 | 40 | # General information about the project. 41 | project = u'pyprocgame' 42 | copyright = u'2010-%d, Adam Preble & Gerry Stellenberg' % (time.gmtime()[0]) 43 | 44 | # The version info for the project you're documenting, acts as replacement for 45 | # |version| and |release|, also used in various other places throughout the 46 | # built documents. 47 | # 48 | import procgame 49 | # The short X.Y version. 50 | version = procgame.__version__ 51 | # The full version, including alpha/beta/rc tags. 52 | release = procgame.__version__ 53 | 54 | # The language for content autogenerated by Sphinx. Refer to documentation 55 | # for a list of supported languages. 56 | #language = None 57 | 58 | # There are two options for replacing |today|: either, you set today to some 59 | # non-false value, then it is used: 60 | #today = '' 61 | # Else, today_fmt is used as the format for a strftime call. 62 | #today_fmt = '%B %d, %Y' 63 | 64 | # List of documents that shouldn't be included in the build. 65 | #unused_docs = [] 66 | 67 | # List of directories, relative to source directory, that shouldn't be searched 68 | # for source files. 69 | exclude_trees = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all documents. 72 | #default_role = None 73 | 74 | # If true, '()' will be appended to :func: etc. cross-reference text. 75 | #add_function_parentheses = True 76 | 77 | # If true, the current module name will be prepended to all description 78 | # unit titles (such as .. function::). 79 | #add_module_names = True 80 | 81 | # If true, sectionauthor and moduleauthor directives will be shown in the 82 | # output. They are ignored by default. 83 | #show_authors = False 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # A list of ignored prefixes for module index sorting. 89 | #modindex_common_prefix = [] 90 | 91 | 92 | # -- Options for HTML output --------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. Major themes that come with 95 | # Sphinx are currently 'default' and 'sphinxdoc'. 96 | html_theme = 'default' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | #html_theme_options = {} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | #html_theme_path = [] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | #html_logo = None 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | #html_favicon = None 121 | 122 | # Add any paths that contain custom static files (such as style sheets) here, 123 | # relative to this directory. They are copied after the builtin static files, 124 | # so a file named "default.css" will overwrite the builtin "default.css". 125 | html_static_path = ['_static'] 126 | 127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 128 | # using the given strftime format. 129 | #html_last_updated_fmt = '%b %d, %Y' 130 | 131 | # If true, SmartyPants will be used to convert quotes and dashes to 132 | # typographically correct entities. 133 | #html_use_smartypants = True 134 | 135 | # Custom sidebar templates, maps document names to template names. 136 | #html_sidebars = {} 137 | 138 | # Additional templates that should be rendered to pages, maps page names to 139 | # template names. 140 | #html_additional_pages = {} 141 | 142 | # If false, no module index is generated. 143 | #html_use_modindex = True 144 | 145 | # If false, no index is generated. 146 | #html_use_index = True 147 | 148 | # If true, the index is split into individual pages for each letter. 149 | #html_split_index = False 150 | 151 | # If true, links to the reST sources are added to the pages. 152 | #html_show_sourcelink = True 153 | 154 | # If true, an OpenSearch description file will be output, and all pages will 155 | # contain a tag referring to it. The value of this option must be the 156 | # base URL from which the finished HTML is served. 157 | #html_use_opensearch = '' 158 | 159 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 160 | #html_file_suffix = '' 161 | 162 | # Output file base name for HTML help builder. 163 | htmlhelp_basename = 'pyprocgamedoc' 164 | 165 | 166 | # -- Options for LaTeX output -------------------------------------------------- 167 | 168 | # The paper size ('letter' or 'a4'). 169 | #latex_paper_size = 'letter' 170 | 171 | # The font size ('10pt', '11pt' or '12pt'). 172 | #latex_font_size = '10pt' 173 | 174 | # Grouping the document tree into LaTeX files. List of tuples 175 | # (source start file, target name, title, author, documentclass [howto/manual]). 176 | latex_documents = [ 177 | ('index', 'pyprocgame.tex', u'pyprocgame Documentation', 178 | u'Adam Preble \\& Gerry Stellenberg', 'manual'), 179 | ] 180 | 181 | # The name of an image file (relative to this directory) to place at the top of 182 | # the title page. 183 | #latex_logo = None 184 | 185 | # For "manual" documents, if this is true, then toplevel headings are parts, 186 | # not chapters. 187 | #latex_use_parts = False 188 | 189 | # Additional stuff for the LaTeX preamble. 190 | #latex_preamble = '' 191 | 192 | # Documents to append as an appendix to all manuals. 193 | #latex_appendices = [] 194 | 195 | # If false, no module index is generated. 196 | #latex_use_modindex = True 197 | -------------------------------------------------------------------------------- /docs/sphinx/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Prerequisites 5 | ------------- 6 | 7 | Before you install pyprocgame, you will need the following software components: 8 | 9 | * `Python 2.6 `_ 10 | * `pypinproc `_ -- native Python extension enabling P-ROC hardware control. Exposes the `libpinproc `_ API to Python and adds native DMD frame manipulation features. See the :ref:`branch note ` below. 11 | * `setuptools `_ -- required for procgame script installation. Also adds easy_install, a helpful installer tool for popular Python modules. 12 | * `pyyaml `_ -- YAML parsing. 13 | * One of the Python graphics and sound modules: 14 | 15 | * `pyglet `_ 16 | * `pygame `_ 17 | 18 | * `Python Imaging Library `_ (PIL) 19 | 20 | Download pyprocgame 21 | ------------------- 22 | 23 | Download the pyprocgame source code from http://github.com/preble/pyprocgame. 24 | 25 | .. _pyprocgame-branch: 26 | 27 | .. note:: 28 | *Which branch should I download?* 29 | The two main branches of pyprocgame are master and dev. Master is the stable branch and is recommended for most users. New features are first made available in the dev branch, but bugs are much more likely to be found in the dev branch. Whichever branch you select, **make sure that libpinproc, pypinproc, and pyprocgame are all from the same branch**! 30 | 31 | 32 | Installing pyprocgame 33 | --------------------- 34 | 35 | Using the command prompt, change to the pyprocgame directory and install pyprocgame with the following command: (depending on your system configuration you may need to use ``sudo``) :: 36 | 37 | python setup.py install 38 | 39 | This will install pyprocgame such that you can import it from any Python script on your system:: 40 | 41 | import procgame.game 42 | 43 | It will also install the "procgame" command line tool into a system-dependent location. On Linux and Mac OS X systems this will probably be in your path such that you can type, from the command line:: 44 | 45 | procgame 46 | 47 | and see a list of available commands. If it is not in your path you can invoke it directly, or modify your PATH environment variable. Note that on Windows the procgame script is typically located in C:\\Python26\\Scripts. 48 | 49 | .. note:: 50 | If you need to modify the pyprocgame source code (most users will not need to do this) you can use the setup.py script to configure pyprocgame to be "installed" at its present location. This allows you use pyprocgame as described above without modifying your Python path, etc. Simply run ``python setup.py develop``. You can reverse this command with ``python setup.py develop --uninstall``. Note that this requires the :mod:`setuptools` module to be installed. 51 | 52 | 53 | .. _config-yaml: 54 | 55 | System Configuration File 56 | ------------------------- 57 | 58 | pyprocgame does not require configuration, but a system configuration file can be used to establish settings specific to your development environment. Note that this is distinct from the machine configuration file, which configures pyprocgame for the hardware elements of the pinball machine. 59 | 60 | The system configuration file is located at :file:`~/.pyprocgame/config.yaml`. It is in the `YAML file format `_, a human-friendly file format. 61 | 62 | .. note:: 63 | On UNIX-like platforms the ``~`` (tilde) is shorthand for the user's home directory. Windows does not understand this shorthand, so if you do not know your home directory, run ``procgame config`` to find your configuration file path. 64 | 65 | Any plain text editor can be used to edit the system configuration file, or you can use the ``procgame`` tool. See :ref:`procgame config ` for more information. 66 | 67 | The system configuration values are processed and accessed by the :mod:`procgame.config` module. 68 | 69 | Configuration Keys/Values 70 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 71 | 72 | +--------------------------+----------+----------------------------------------------------+ 73 | | Top Level Key | Type | Description | 74 | +==========================+==========+====================================================+ 75 | | ``config_path`` | Sequence | List of paths that will be searched for machine | 76 | | | | configuration files via | 77 | | | | :meth:`~procgame.game.GameController.load_config`. | 78 | +--------------------------+----------+----------------------------------------------------+ 79 | | ``desktop_dmd_scale`` | Number | (pyglet :class:`~procgame.desktop.Desktop` only) | 80 | | | | Sets the scale factor of the desktop DMD display. | 81 | +--------------------------+----------+----------------------------------------------------+ 82 | | ``dmd_cache_path`` | String | Provide a path to the directory to store cached | 83 | | | | animations in. If this key is not present, no | 84 | | | | images will be cached. See | 85 | | | | :meth:`procgame.dmd.Animation.load` for further | 86 | | | | details. | 87 | +--------------------------+----------+----------------------------------------------------+ 88 | | ``font_path`` | Sequence | List of paths that will be searched by | 89 | | | | :meth:`procgame.dmd.font_named`. | 90 | +--------------------------+----------+----------------------------------------------------+ 91 | | ``keyboard_switch_map`` | Mapping | Maps characters (keys) to switches (values); used | 92 | | | | by :class:`~procgame.desktop.Desktop` to interpret | 93 | | | | keypresses as switch events. Switch values are | 94 | | | | run through :meth:`pinproc.decode`. | 95 | +--------------------------+----------+----------------------------------------------------+ 96 | | ``pinproc_class`` | String | Full name of a class to use as a standin for the | 97 | | | | :class:`~pinproc.PinPROC` class. Typically used | 98 | | | | with :class:`procgame.fakepinproc.FakePinPROC`. | 99 | +--------------------------+----------+----------------------------------------------------+ 100 | 101 | 102 | Example Configuration 103 | ^^^^^^^^^^^^^^^^^^^^^ 104 | 105 | :: 106 | 107 | font_path: 108 | - . 109 | - ~/Projects/PROC/shared/dmd 110 | pinproc_class: procgame.fakepinproc.FakePinPROC 111 | config_path: 112 | - ~/Projects/PROC/shared/config 113 | keyboard_switch_map: 114 | # Enter, Up, Down, Exit 115 | 7: SD8 116 | 8: SD7 117 | 9: SD6 118 | 0: SD5 119 | # Start: 120 | s: S13 121 | z: SF4 122 | /: SF2 123 | desktop_dmd_scale: 2 124 | dmd_cache_path: ~/.pyprocgame/dmd_cache 125 | -------------------------------------------------------------------------------- /procgame/modes/drops.py: -------------------------------------------------------------------------------- 1 | from ..game import Mode 2 | 3 | 4 | class Scoring_Mode(Mode): 5 | """Scoring_mode base class. Useful for modes that result in bonus points.""" 6 | 7 | def __init__(self, game, priority): 8 | super(Scoring_Mode, self).__init__(game, priority) 9 | self.bonus_base_elements = {} 10 | self.bonus_x = 1 11 | 12 | 13 | class BasicDropTargetBank(Mode): 14 | """Basic Drop Target Bank mode.""" 15 | 16 | def __init__(self, game, priority, prefix, letters): 17 | super(BasicDropTargetBank, self).__init__(game, 8) 18 | self.letters = letters 19 | self.prefix = prefix 20 | self.auto_reset = True 21 | self.on_completed = None 22 | self.on_advance = None 23 | self.state = {} 24 | self.paused = False 25 | # Ordinarily a mode would have sw_switchName_open() handlers, 26 | # but because this is a generic Mode we will configure them 27 | # programatically to all call the dropped() method: 28 | for name in self.names(): 29 | self.add_switch_handler(name=name, event_type='open', delay=None, handler=self.dropped) 30 | self.state[name] = 'down' 31 | 32 | def mode_started(self): 33 | self.animated_reset(seconds=1.0) 34 | 35 | def dropped(self, sw): 36 | """General handler for all drop target switches""" 37 | # if self.all_down(): 38 | # self.game.set_status("ALL DOWN and %s:\n%s" % (self.all_were_down, str.join('', traceback.format_stack()))) 39 | if not self.paused and self.state[sw.name] == 'up': 40 | self.game.lamps[sw.name].schedule(schedule=0xf0f0f0f0, cycle_seconds=1, now=True) 41 | self.state[sw.name] = 'down' 42 | if self.all_down(): 43 | self.on_completed(self) 44 | if self.auto_reset: 45 | self.animated_reset(seconds=2.0) 46 | else: 47 | self.on_advance(self) 48 | 49 | def chase_lamps(self): 50 | """Perform an animation using the lamps.""" 51 | bits = 3 52 | schedule = ~(0xffffffff << bits) 53 | for name in self.names(): 54 | self.game.lamps[name].schedule(schedule=(schedule | (schedule << 16)), cycle_seconds=4, now=True) 55 | schedule <<= bits 56 | 57 | def animated_reset(self, seconds): 58 | """Perform an animation using the lamps and then reset the drop targets.""" 59 | self.chase_lamps() 60 | self.schedule_delayed_reset(seconds) 61 | 62 | def schedule_delayed_reset(self, delay): 63 | """Schedule a call to reset_drop_target_bank() for 'delay' seconds in the future.""" 64 | self.delay(name='reset', event_type=None, delay=delay, handler=self.reset_drop_target_bank) 65 | 66 | def reset_drop_target_bank(self): 67 | """Resets the drop targets to the up position and lights each of the lamps.""" 68 | self.game.coils.resetDropTarget.pulse(30) 69 | for name in self.names(): 70 | self.game.lamps[name].enable() 71 | self.state[name] = 'up' 72 | 73 | def update_lamps(self): 74 | for name in self.names(): 75 | if self.state[name] == 'up': 76 | self.game.lamps[name].enable() 77 | 78 | def all_down(self): 79 | """Returns True if all of the drop targets are down.""" 80 | for name in self.names(): 81 | if self.state[name] == 'up': 82 | return False 83 | # if self.game.switches[name].is_closed(): 84 | # return False 85 | return True 86 | 87 | def names(self): 88 | """Returns the drop target switch/lamp names in order.""" 89 | for letter in self.letters: 90 | yield self.prefix + letter 91 | 92 | 93 | class ProgressiveDropTargetBank(BasicDropTargetBank): 94 | """Implements a drop target bank that requires that the targets be hit in order. 95 | The advance_switch argument should be the name of the switch that, when closed, causes the current target to advance.""" 96 | 97 | def __init__(self, game, priority, prefix, letters, advance_switch): 98 | super(ProgressiveDropTargetBank, self).__init__(game, priority, prefix, letters) 99 | self.add_switch_handler(name=advance_switch, event_type='closed', delay=None, handler=self.__advance_triggered) 100 | self.advance_switch = self.game.switches[advance_switch] 101 | self.current_target = None # Set by animated_reset() on mode start. 102 | 103 | def advance(self): 104 | """Advances the current target to the next target. 105 | If the last target is active, the bank will perform an animated reset. 106 | If all of the targets are down the bank will be physically reset but the current target will be advanced normally.""" 107 | use_next = False 108 | new_target = None 109 | for letter in self.letters: 110 | name = self.prefix + letter 111 | if use_next: 112 | new_target = name 113 | break 114 | if self.current_target == name: 115 | use_next = True 116 | if new_target is None: 117 | # All of them must be down! 118 | self.on_completed(self) 119 | if self.auto_reset: 120 | self.animated_reset(2.0) 121 | else: 122 | self.on_advance(self) 123 | self.game.lamps[self.current_target].enable() 124 | self.current_target = new_target 125 | self.game.lamps[self.current_target].schedule(schedule=0xf0f0f0f0, cycle_seconds=0, now=True) 126 | if self.all_down() and self.auto_reset: 127 | self.reset_drop_target_bank() 128 | 129 | def dropped(self, sw): 130 | """General handler for all individual drop target switch events. 131 | Advances the current target if it was hit. 132 | Otherwise it advances and physically resets the bank if all targets are now down.""" 133 | if not self.paused: 134 | self.advance() # Given how advance() works, this is probably a sufficient replacement for the below: 135 | # if sw.name == self.current_target: 136 | # self.advance() 137 | # elif self.all_down(): 138 | # self.advance() 139 | # self.reset_drop_target_bank() 140 | 141 | def animated_reset(self, seconds): 142 | """Performs an animated reset and sets the current target back to the first target.""" 143 | self.current_target = self.prefix + self.letters[0] 144 | super(ProgressiveDropTargetBank, self).animated_reset(seconds) 145 | 146 | def reset_drop_target_bank(self): 147 | """Resets the drop targets to the up position and configures the lamps to reflect the current target state.""" 148 | self.game.coils.resetDropTarget.pulse() 149 | before = True 150 | for name in self.names(): 151 | if name == self.current_target: 152 | self.game.lamps[name].schedule(schedule=0xf0f0f0f0, cycle_seconds=0, now=True) 153 | before = False 154 | elif before: 155 | self.game.lamps[name].enable() 156 | else: 157 | self.game.lamps[name].disable() 158 | 159 | def __advance_triggered(self, sw): 160 | """Switch event handler for the advance_switch configured on mode creation.""" 161 | self.advance() 162 | -------------------------------------------------------------------------------- /procgame/highscore/entry.py: -------------------------------------------------------------------------------- 1 | from ..dmd import dmd 2 | from ..dmd.font import font_named 3 | from ..dmd.layers import * 4 | from ..game import Mode 5 | 6 | 7 | class InitialEntryMode(Mode): 8 | """Mode that prompts the player for their initials. 9 | 10 | *left_text* and *right_text* are strings or arrays to be displayed at the 11 | left and right corners of the display. If they are arrays they will be 12 | rotated. 13 | 14 | :attr:`entered_handler` is called once the initials have been confirmed. 15 | 16 | This mode does not remove itself; this should be done in *entered_handler*.""" 17 | 18 | entered_handler = None 19 | """Method taking two parameters: `mode` and `inits`.""" 20 | 21 | char_back = '<' 22 | char_done = '=' 23 | 24 | init_font = None 25 | font = None 26 | letters_font = None 27 | 28 | def __init__(self, game, priority, left_text, right_text, entered_handler): 29 | super(InitialEntryMode, self).__init__(game, priority) 30 | 31 | self.entered_handler = entered_handler 32 | 33 | self.init_font = font_named('Font09Bx7.dmd') 34 | self.font = font_named('Font07x5.dmd') 35 | self.letters_font = font_named('Font07x5.dmd') 36 | 37 | self.layer = GroupedLayer(128, 32) 38 | self.layer.opaque = True 39 | self.layer.layers = [] 40 | 41 | if type(right_text) != list: 42 | right_text = [right_text] 43 | if type(left_text) != list: 44 | left_text = [left_text] 45 | 46 | seconds_per_text = 1.5 47 | 48 | script = [] 49 | for text in left_text: 50 | frame = dmd.Frame(width=128, height=8) 51 | self.font.draw(frame, text, 0, 0) 52 | script.append({'seconds': seconds_per_text, 'layer': FrameLayer(frame=frame)}) 53 | topthird_left_layer = ScriptedLayer(width=128, height=8, script=script) 54 | topthird_left_layer.composite_op = 'blacksrc' 55 | self.layer.layers += [topthird_left_layer] 56 | 57 | script = [] 58 | for text in right_text: 59 | frame = dmd.Frame(width=128, height=8) 60 | self.font.draw(frame, text, 128 - (self.font.size(text)[0]), 0) 61 | script.append({'seconds': seconds_per_text, 'layer': dmd.FrameLayer(frame=frame)}) 62 | topthird_right_layer = ScriptedLayer(width=128, height=8, script=script) 63 | topthird_right_layer.composite_op = 'blacksrc' 64 | self.layer.layers += [topthird_right_layer] 65 | 66 | self.inits_frame = dmd.Frame(width=128, height=10) 67 | inits_layer = FrameLayer(opaque=False, frame=self.inits_frame) 68 | inits_layer.set_target_position(0, 11) 69 | self.layer.layers += [inits_layer] 70 | 71 | self.lowerhalf_layer = FrameQueueLayer(opaque=False, hold=True) 72 | self.lowerhalf_layer.set_target_position(0, 24) 73 | self.layer.layers += [self.lowerhalf_layer] 74 | 75 | self.letters = [] 76 | for idx in range(26): 77 | self.letters += [chr(ord('A') + idx)] 78 | self.letters += [' ', '.', self.char_back, self.char_done] 79 | self.current_letter_index = 0 80 | self.inits = self.letters[self.current_letter_index] 81 | self.animate_to_index(0) 82 | 83 | def mode_started(self): 84 | pass 85 | 86 | def mode_stopped(self): 87 | pass 88 | 89 | def animate_to_index(self, new_index, inc=0): 90 | letter_spread = 10 91 | letter_width = 7 92 | if inc < 0: 93 | rng = range(inc * letter_spread, 1) 94 | elif inc > 0: 95 | rng = range(inc * letter_spread)[::-1] 96 | else: 97 | rng = [0] 98 | # print rng 99 | for x in rng: 100 | frame = dmd.Frame(width=128, height=10) 101 | for offset in range(-7, 8): 102 | index = new_index - offset 103 | # print "Index %d len=%d" % (index, len(self.letters)) 104 | if index < 0: 105 | index = len(self.letters) + index 106 | elif index >= len(self.letters): 107 | index = index - len(self.letters) 108 | (w, h) = self.font.size(self.letters[index]) 109 | # print "Drawing %d w=%d" % (index, w) 110 | self.letters_font.draw(frame, self.letters[index], 111 | 128 / 2 - offset * letter_spread - letter_width / 2 + x, 0) 112 | frame.fill_rect(64 - 5, 0, 1, 10, 1) 113 | frame.fill_rect(64 + 5, 0, 1, 10, 1) 114 | self.lowerhalf_layer.frames += [frame] 115 | self.current_letter_index = new_index 116 | 117 | # Prune down the frames list so we don't get too far behind while animating 118 | x = 0 119 | while len(self.lowerhalf_layer.frames) > 15 and x < (len(self.lowerhalf_layer.frames) - 1): 120 | del self.lowerhalf_layer.frames[x] 121 | x += 2 122 | 123 | # Now draw the top right panel, with the selected initials in order: 124 | self.inits_frame.clear() 125 | init_spread = 8 126 | x_offset = self.inits_frame.width / 2 - len(self.inits) * init_spread / 2 127 | for x in range(len(self.inits)): 128 | self.init_font.draw(self.inits_frame, self.inits[x], x * init_spread + x_offset, 0) 129 | self.inits_frame.fill_rect((len(self.inits) - 1) * init_spread + x_offset, 9, 8, 1, 1) 130 | 131 | def letter_increment(self, inc): 132 | new_index = (self.current_letter_index + inc) 133 | if new_index < 0: 134 | new_index = len(self.letters) + new_index 135 | elif new_index >= len(self.letters): 136 | new_index = new_index - len(self.letters) 137 | # print("letter_increment %d + %d = %d" % (self.current_letter_index, inc, new_index)) 138 | self.inits = self.inits[:-1] + self.letters[new_index] 139 | self.animate_to_index(new_index, inc) 140 | 141 | def letter_accept(self): 142 | # TODO: Add 'back'/erase/end 143 | letter = self.letters[self.current_letter_index] 144 | if letter == self.char_back: 145 | if len(self.inits) > 0: 146 | self.inits = self.inits[:-1] 147 | elif letter == self.char_done or len(self.inits) > 10: 148 | self.inits = self.inits[:-1] # Strip off the done character 149 | if self.entered_handler is not None: 150 | self.entered_handler(mode=self, inits=self.inits) 151 | else: 152 | self.game.logger.warning('InitialEntryMode finished but no entered_handler to notify!') 153 | else: 154 | self.inits += letter 155 | self.letter_increment(0) 156 | 157 | def sw_flipperLwL_active(self, sw): 158 | self.periodic_left() 159 | return False 160 | 161 | def sw_flipperLwL_inactive(self, sw): 162 | self.cancel_delayed('periodic_movement') 163 | 164 | def sw_flipperLwR_active(self, sw): 165 | self.periodic_right() 166 | return False 167 | 168 | def sw_flipperLwR_inactive(self, sw): 169 | self.cancel_delayed('periodic_movement') 170 | 171 | def periodic_left(self): 172 | self.letter_increment(-1) 173 | self.delay(name='periodic_movement', event_type=None, delay=0.2, handler=self.periodic_left) 174 | 175 | def periodic_right(self): 176 | self.letter_increment(1) 177 | self.delay(name='periodic_movement', event_type=None, delay=0.2, handler=self.periodic_right) 178 | 179 | def sw_startButton_active(self, sw): 180 | self.letter_accept() 181 | return True 182 | -------------------------------------------------------------------------------- /procgame/dmd/font.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from procgame import config 4 | from procgame import util 5 | from procgame.dmd import Animation, Frame 6 | import sys 7 | 8 | # Anchor values are used by Font.draw_in_rect(): 9 | AnchorN = 1 10 | AnchorW = 2 11 | AnchorE = 4 12 | AnchorS = 8 13 | AnchorNE = AnchorN | AnchorE 14 | AnchorNW = AnchorN | AnchorW 15 | AnchorSE = AnchorS | AnchorE 16 | AnchorSW = AnchorS | AnchorW 17 | AnchorCenter = 0 18 | 19 | 20 | class Font(object): 21 | """Variable-width bitmap font. 22 | 23 | Fonts can be loaded manually, using :meth:`load`, or with the :func:`font_named` utility function 24 | which supports searching a font path.""" 25 | 26 | char_widths = None 27 | """Array of dot widths for each character, 0-indexed from . 28 | This array is populated by :meth:`load`. You may alter this array 29 | in order to update the font and then :meth:`save` it.""" 30 | 31 | tracking = 0 32 | """Number of dots to adjust the horizontal position between characters, in addition to the last character's width.""" 33 | 34 | composite_op = 'copy' 35 | """Composite operation used by :meth:`draw` when calling :meth:`~pinproc.DMDBuffer.copy_rect`.""" 36 | 37 | def __init__(self, filename=None): 38 | super(Font, self).__init__() 39 | self.__anim = Animation() 40 | self.char_size = None 41 | self.bitmap = None 42 | if filename is not None: 43 | self.load(filename) 44 | 45 | def load(self, filename): 46 | """Loads the font from a ``.dmd`` file (see :meth:`Animation.load`). 47 | Fonts are stored in .dmd files with frame 0 containing the bitmap data 48 | and frame 1 containing the character widths. 96 characters (32..127, 49 | ASCII printables) are stored in a 10x10 grid, starting with space (``' '``) 50 | in the upper left at 0, 0. The character widths are stored in the second frame 51 | within the 'raw' bitmap data in bytes 0-95. 52 | """ 53 | self.__anim.load(filename) 54 | if self.__anim.width != self.__anim.height: 55 | raise ValueError("Width != height!") 56 | if len(self.__anim.frames) == 1: 57 | # We allow 1 frame for handmade fonts. 58 | # This is so that they can be loaded as a basic bitmap, have their char widths modified, and then be saved. 59 | print("Font animation file %s has 1 frame; adding one" % filename) 60 | self.__anim.frames += [Frame(self.__anim.width, self.__anim.height)] 61 | elif len(self.__anim.frames) != 2: 62 | raise ValueError("Expected 2 frames: %d" % len(self.__anim.frames)) 63 | self.char_size = self.__anim.width / 10 64 | self.bitmap = self.__anim.frames[0] 65 | self.char_widths = [] 66 | for i in range(96): 67 | self.char_widths += [self.__anim.frames[1].get_dot(i % self.__anim.width, i / self.__anim.width)] 68 | return self 69 | 70 | def save(self, filename): 71 | """Save the font to the given path.""" 72 | out = Animation() 73 | out.width = self.__anim.width 74 | out.height = self.__anim.height 75 | out.frames = [self.bitmap, Frame(out.width, out.height)] 76 | for i in range(96): 77 | out.frames[1].set_dot(i % self.__anim.width, i / self.__anim.width, self.char_widths[i]) 78 | out.save(filename) 79 | 80 | def draw(self, frame, text, x, y): 81 | """Uses this font's characters to draw the given string at the given position.""" 82 | for ch in text: 83 | char_offset = ord(ch) - ord(' ') 84 | if char_offset < 0 or char_offset >= 96: 85 | continue 86 | char_x = self.char_size * (char_offset % 10) 87 | char_y = self.char_size * (char_offset / 10) 88 | width = self.char_widths[char_offset] 89 | Frame.copy_rect(dst=frame, dst_x=x, dst_y=y, src=self.bitmap, src_x=char_x, src_y=char_y, width=width, 90 | height=self.char_size, op=self.composite_op) 91 | x += width + self.tracking 92 | return x 93 | 94 | def size(self, text): 95 | """Returns a tuple of the width and height of this text as rendered with this font.""" 96 | x = 0 97 | for ch in text: 98 | char_offset = ord(ch) - ord(' ') 99 | if char_offset < 0 or char_offset >= 96: 100 | continue 101 | width = self.char_widths[char_offset] 102 | x += width + self.tracking 103 | return x, self.char_size 104 | 105 | def draw_in_rect(self, frame, text, rect=(0, 0, 128, 32), anchor=AnchorCenter): 106 | """Draw *text* on *frame* within the given *rect*, aligned in accordance with *anchor*. 107 | 108 | *rect* is a tuple of length 4: (origin_x, origin_y, height, width). 0,0 is in the upper left (NW) corner. 109 | 110 | *anchor* is one of: 111 | :attr:`~procgame.dmd.AnchorN`, 112 | :attr:`~procgame.dmd.AnchorE`, 113 | :attr:`~procgame.dmd.AnchorS`, 114 | :attr:`~procgame.dmd.AnchorW`, 115 | :attr:`~procgame.dmd.AnchorNE`, 116 | :attr:`~procgame.dmd.AnchorNW`, 117 | :attr:`~procgame.dmd.AnchorSE`, 118 | :attr:`~procgame.dmd.AnchorSW`, or 119 | :attr:`~procgame.dmd.AnchorCenter` (the default). 120 | """ 121 | origin_x, origin_y, width, height = rect 122 | text_width, text_height = self.size(text) 123 | x = 0 124 | y = 0 125 | 126 | # print "Size: %d x %d" % (text_height) 127 | 128 | if anchor & AnchorN: 129 | y = origin_y 130 | elif anchor & AnchorS: 131 | y = origin_y + (height - text_height) 132 | else: 133 | y = origin_y + (height / 2.0 - text_height / 2.0) 134 | 135 | if anchor & AnchorW: 136 | x = origin_x 137 | elif anchor & AnchorE: 138 | x = origin_x + (width - text_width) 139 | else: 140 | x = origin_x + (width / 2.0 - text_width / 2.0) 141 | 142 | self.draw(frame=frame, text=text, x=x, y=y) 143 | 144 | 145 | font_path = [] 146 | """Array of paths that will be searched by :meth:`~procgame.dmd.font_named` to locate fonts. 147 | 148 | When this module is initialized the pyprocgame global configuration (:attr:`procgame.config.values`) 149 | ``font_path`` key path is used to initialize this array.""" 150 | 151 | 152 | def init_font_path(): 153 | global font_path 154 | try: 155 | value = config.value_for_key_path('font_path') 156 | if issubclass(type(value), list): 157 | font_path.extend(map(os.path.expanduser, value)) 158 | elif issubclass(type(value), str): 159 | font_path.append(os.path.expanduser(value)) 160 | elif value is None: 161 | print('WARNING no font_path set in %s!' % (config.path)) 162 | else: 163 | print('ERROR loading font_path from %s; type is %s but should be list or str.' % (config.path, type(value))) 164 | sys.exit(1) 165 | except ValueError as e: 166 | # print e 167 | pass 168 | 169 | 170 | init_font_path() 171 | 172 | __font_cache = {} 173 | 174 | 175 | def font_named(name): 176 | """Searches the :attr:`font_path` for a font file of the given name and returns an instance of :class:`Font` if it exists.""" 177 | if name in __font_cache: 178 | return __font_cache[name] 179 | path = util.find_file_in_path(name, font_path) 180 | if path: 181 | font = Font(path) 182 | __font_cache[name] = font 183 | return font 184 | else: 185 | raise ValueError('Font named "%s" not found; font_path=%s. Have you configured font_path in config.yaml?' % ( 186 | name, font_path)) 187 | -------------------------------------------------------------------------------- /procgame/modes/scoredisplay.py: -------------------------------------------------------------------------------- 1 | import locale 2 | 3 | from .. import dmd 4 | from ..game import Mode 5 | 6 | 7 | class ScoreLayer(dmd.GroupedLayer): 8 | def __init__(self, width, height, mode): 9 | super(ScoreLayer, self).__init__(width, height, mode) 10 | self.mode = mode 11 | 12 | def next_frame(self): 13 | """docstring for next_frame""" 14 | # Setup for the frame. 15 | self.mode.update_layer() 16 | return super(ScoreLayer, self).next_frame() 17 | 18 | 19 | class ScoreDisplay(Mode): 20 | """:class:`ScoreDisplay` is a mode that provides a DMD layer containing a generic 1-to-4 player score display. 21 | To use :class:`ScoreDisplay` simply instantiate it and add it to the mode queue. A low priority is recommended. 22 | 23 | When the layer is asked for its :meth:`~procgame.dmd.Layer.next_frame` the DMD frame is built based on 24 | the player score and ball information contained in the :class:`~procgame.game.GameController`. 25 | 26 | :class:`ScoreDisplay` uses a number of fonts, the defaults of which are included in the shared DMD resources folder. 27 | If a font cannot be found then the score may not display properly 28 | in some states. Fonts are loaded using :func:`procgame.dmd.font_named`; see its documentation for dealing with 29 | fonts that cannot be found. 30 | 31 | You can substitute your own fonts (of the appropriate size) by assigning the font attributes after initializing 32 | :class:`ScoreDisplay`. 33 | """ 34 | 35 | font_common = None 36 | """Font used for the bottom status line text: ``'BALL 1 FREE PLAY'``. Defaults to Font07x5.dmd.""" 37 | font_18x12 = None 38 | """Defaults to Font18x12.dmd.""" 39 | font_18x11 = None 40 | """Defaults to Font18x11.dmd.""" 41 | font_18x10 = None 42 | """Defaults to Font18x10.dmd.""" 43 | font_14x10 = None 44 | """Defaults to Font14x10.dmd.""" 45 | font_14x9 = None 46 | """Defaults to Font14x9.dmd.""" 47 | font_14x8 = None 48 | """Defaults to Font14x8.dmd.""" 49 | font_09x5 = None 50 | """Defaults to Font09x5.dmd.""" 51 | font_09x6 = None 52 | """Defaults to Font09x6.dmd.""" 53 | font_09x7 = None 54 | """Defaults to Font09x7.dmd.""" 55 | 56 | credit_string_callback = None 57 | """If non-``None``, :meth:`update_layer` will call it with no parameters to get the credit string (usually FREE PLAY or CREDITS 1 or similar). 58 | If this method returns the empty string no text will be shown (and any ball count will be centered). If ``None``, FREE PLAY will be shown.""" 59 | 60 | def __init__(self, game, priority, left_players_justify="right"): 61 | super(ScoreDisplay, self).__init__(game, priority) 62 | self.layer = ScoreLayer(128, 32, self) 63 | self.font_common = dmd.font_named("Font07x5.dmd") 64 | self.font_18x12 = dmd.font_named("Font18x12.dmd") 65 | self.font_18x11 = dmd.font_named("Font18x11.dmd") 66 | self.font_18x10 = dmd.font_named("Font18x10.dmd") 67 | self.font_14x10 = dmd.font_named("Font14x10.dmd") 68 | self.font_14x9 = dmd.font_named("Font14x9.dmd") 69 | self.font_14x8 = dmd.font_named("Font14x8.dmd") 70 | self.font_09x5 = dmd.font_named("Font09x5.dmd") 71 | self.font_09x6 = dmd.font_named("Font09x6.dmd") 72 | self.font_09x7 = dmd.font_named("Font09x7.dmd") 73 | self.set_left_players_justify(left_players_justify) 74 | 75 | def set_left_players_justify(self, left_players_justify): 76 | """Call to set the justification of the left-hand players' scores in a multiplayer game. 77 | Valid values for ``left_players_justify`` are ``'left'`` and ``'right'``.""" 78 | if left_players_justify == "left": 79 | self.score_posns = {True: [(0, 0), (128, 0), (0, 11), (128, 11)], 80 | False: [(0, -1), (128, -1), (0, 16), (128, 16)]} 81 | elif left_players_justify == "right": 82 | self.score_posns = {True: [(75, 0), (128, 0), (75, 11), (128, 11)], 83 | False: [(52, -1), (128, -1), (52, 16), (128, 16)]} 84 | else: 85 | raise ValueError("Justify must be right or left.") 86 | self.score_justs = [left_players_justify, 'right', left_players_justify, 'right'] 87 | 88 | def format_score(self, score): 89 | """Returns a string representation of the given score value. 90 | Override to customize the display of numeric score values.""" 91 | if score == 0: 92 | return '00' 93 | else: 94 | return locale.format_string("%d", score, True) 95 | 96 | def font_for_score_single(self, score): 97 | """Returns the font to be used for displaying the given numeric score value in a single-player game.""" 98 | if score < 1e10: 99 | return self.font_18x12 100 | elif score < 1e11: 101 | return self.font_18x11 102 | else: 103 | return self.font_18x10 104 | 105 | def font_for_score(self, score, is_active_player): 106 | """Returns the font to be used for displaying the given numeric score value in a 2, 3, or 4-player game.""" 107 | if is_active_player: 108 | if score < 1e7: 109 | return self.font_14x10 110 | if score < 1e8: 111 | return self.font_14x9 112 | else: 113 | return self.font_14x8 114 | else: 115 | if score < 1e7: 116 | return self.font_09x7 117 | if score < 1e8: 118 | return self.font_09x6 119 | else: 120 | return self.font_09x5 121 | 122 | def pos_for_player(self, player_index, is_active_player): 123 | return self.score_posns[is_active_player][player_index] 124 | 125 | def justify_for_player(self, player_index): 126 | return self.score_justs[player_index] 127 | 128 | def update_layer(self): 129 | """Called by the layer to update the score layer for the present game state.""" 130 | self.layer.layers = [] 131 | if len(self.game.players) <= 1: 132 | self.update_layer_1p() 133 | else: 134 | self.update_layer_4p() 135 | # Common: Add the "BALL X ... FREE PLAY" footer. 136 | common = dmd.TextLayer(128 / 2, 32 - 6, self.font_common, "center") 137 | 138 | credit_str = 'FREE PLAY' 139 | if self.credit_string_callback: 140 | credit_str = self.credit_string_callback() 141 | if self.game.ball == 0: 142 | common.set_text(credit_str) 143 | elif len(credit_str) > 0: 144 | common.set_text("BALL %d %s" % (self.game.ball, credit_str)) 145 | else: 146 | common.set_text("BALL %d" % (self.game.ball)) 147 | self.layer.layers += [common] 148 | 149 | def update_layer_1p(self): 150 | if self.game.current_player() is None: 151 | score = 0 # Small hack to make *something* show up on startup. 152 | else: 153 | score = self.game.current_player().score 154 | layer = dmd.TextLayer(128 / 2, 5, self.font_for_score_single(score), "center") 155 | layer.set_text(self.format_score(score)) 156 | self.layer.layers += [layer] 157 | 158 | def update_layer_4p(self): 159 | for i in range(len(self.game.players[:4])): # Limit to first 4 players for now. 160 | score = self.game.players[i].score 161 | is_active_player = (self.game.ball > 0) and (i == self.game.current_player_index) 162 | font = self.font_for_score(score=score, is_active_player=is_active_player) 163 | pos = self.pos_for_player(player_index=i, is_active_player=is_active_player) 164 | justify = self.justify_for_player(player_index=i) 165 | layer = dmd.TextLayer(pos[0], pos[1], font, justify) 166 | layer.set_text(self.format_score(score)) 167 | self.layer.layers += [layer] 168 | pass 169 | 170 | def mode_started(self): 171 | pass 172 | 173 | def mode_stopped(self): 174 | pass 175 | --------------------------------------------------------------------------------