summary refs log tree commit diff homepage
diff options
context:
space:
mode:
authorNguyễn Gia Phong <vn.mcsinyx@gmail.com>2018-02-13 20:51:41 +0700
committerNguyễn Gia Phong <vn.mcsinyx@gmail.com>2018-02-13 20:51:41 +0700
commit6a4f7e47aed7510e8e45bddf1308fe4eeb87534d (patch)
treebe6cf79ce70194b02b0ad875f1688e493c86ef04
parent465be4493db11a458ae05c1be67bac109c753fb0 (diff)
downloadbrutalmaze-6a4f7e47aed7510e8e45bddf1308fe4eeb87534d.tar.gz
Add argument parser
-rw-r--r--brutalmaze/__init__.py4
-rw-r--r--brutalmaze/constants.py11
-rw-r--r--brutalmaze/main.py199
-rwxr-xr-xsetup.py4
4 files changed, 140 insertions, 78 deletions
diff --git a/brutalmaze/__init__.py b/brutalmaze/__init__.py
index 75b07c7..46e1d6d 100644
--- a/brutalmaze/__init__.py
+++ b/brutalmaze/__init__.py
@@ -1,5 +1,3 @@
-"""Brutal Maze is a hash and slash game with fast-paced action and a
-minimalist art style.
-"""
+"""Brutal Maze is a minimalist hack and slash game with fast-paced action"""
 
 from .main import main
diff --git a/brutalmaze/constants.py b/brutalmaze/constants.py
index c30488a..d7f3630 100644
--- a/brutalmaze/constants.py
+++ b/brutalmaze/constants.py
@@ -19,21 +19,10 @@
 
 __doc__ = 'brutalmaze module for shared constants'
 
-from os.path import join
-
-from appdirs import user_config_dir, site_config_dir
 from pkg_resources import resource_filename
 from pygame import image
 from pygame.mixer import Sound
 
-USER_CONFIG = join(user_config_dir('brutalmaze'), 'settings.ini')
-SITE_CONFIG = join(site_config_dir('brutalmaze'), 'settings.ini')
-DEFAULT_BINDINGS = {'New game': 'F2', 'Pause': 'p',
-                    'Move left': 'Left', 'Move right': 'Right',
-                    'Move up': 'Up', 'Move down': 'Down',
-                    'Long-range attack': 'Mouse1',
-                    'Close-range attack': 'Mouse3'}
-
 ICON = image.load(resource_filename('brutalmaze', 'icon.png'))
 MUSIC = resource_filename('brutalmaze', 'soundfx/music.ogg')
 SFX_SPAWN = resource_filename('brutalmaze', 'soundfx/spawn.ogg')
diff --git a/brutalmaze/main.py b/brutalmaze/main.py
index e1d5279..1bcf3d1 100644
--- a/brutalmaze/main.py
+++ b/brutalmaze/main.py
@@ -17,76 +17,149 @@
 #
 # Copyright (C) 2017 Nguyễn Gia Phong
 
+__version__ = '0.5.1'
+
 import re
+from argparse import ArgumentParser, RawTextHelpFormatter
 from collections import deque
 try:                    # Python 3
     from configparser import ConfigParser, NoOptionError, NoSectionError
 except ImportError:     # Python 2
     from ConfigParser import ConfigParser, NoOptionError, NoSectionError
+from os.path import join, pathsep
+
 
 import pygame
 from pygame import DOUBLEBUF, KEYDOWN, OPENGL, QUIT, RESIZABLE, VIDEORESIZE
+from appdirs import AppDirs
 
-from .constants import USER_CONFIG, SITE_CONFIG, DEFAULT_BINDINGS, ICON, MUSIC
+from .constants import *
 from .maze import Maze
 
 
-def getconf(config, section, option, valtype=str, fallback=None):
-    """Return an option value for a given section from a ConfigParser
-    object.
-
-    If the key is not found, return the fallback value.
+class ConfigReader:
+    """Object reading and processing INI configuration file for
+    Brutal Maze.
     """
-    if fallback is None: fallback = valtype()
-    try:
-        if valtype == str:
-            return config.get(section, option)
-        elif valtype == bool:
-            return config.getboolean(section, option)
-        elif valtype == float:
-            return config.getfloat(section, option)
-        elif valtype == int:
-            return config.getint(section, option)
-    except NoSectionError, NoOptionError:
-        return fallback
+    DEFAULT_BINDINGS = (('New game', 'new', 'F2'),
+                        ('Pause', 'pause', 'p'),
+                        ('Move left', 'left', 'Left'),
+                        ('Move right', 'right', 'Right'),
+                        ('Move up', 'up', 'Up'),
+                        ('Move down', 'down', 'Down'),
+                        ('Long-range attack', 'shot', 'Mouse1'),
+                        ('Close-range attack', 'slash', 'Mouse3'))
+    WEIRD_MOUSE_ERR = '{}: Mouse is not a suitable control'
+    INVALID_CONTROL_ERR = '{}: {} is not recognized as a valid control key'
+
+    def __init__(self, filenames):
+        self.config = ConfigParser()
+        self.config.read(filenames)
+
+    def _getconf(self, section, option, val_t):
+        if val_t == str:
+            return self.config.get(section, option)
+        elif val_t == bool:
+            return self.config.getboolean(section, option)
+        elif val_t == float:
+            return self.config.getfloat(section, option)
+        elif val_t == int:
+            return self.config.getint(section, option)
+
+    def getconf(self, section, option, val_t=str, fallback=None):
+        """Return the value of the option in the given section.
+
+        If the value is not found, return fallback.
+        """
+        try:
+            return self._getconf(section, option, val_t)
+        except NoSectionError, NoOptionError:
+            return val_t() if fallback is None else fallback
+
+    def setconf(self, name, section, option, val_t=str, fallback=None):
+        """Set the named attribute to the value of the option in
+        the given section.
+
+        If the value is not found and attribute does not exist,
+        use fallback.
+        """
+        try:
+            setattr(self, name, self._getconf(section, option, val_t))
+        except NoSectionError, NoOptionError:
+            try:
+                getattr(self, name)
+            except AttributeError:
+                setattr(self, name, val_t() if fallback is None else fallback)
+
+    def parse_graphics(self):
+        """Parse graphics configurations."""
+        self.setconf('width', 'Graphics', 'Screen width', int, 640)
+        self.setconf('height', 'Graphics', 'Screen height', int, 480)
+        self.setconf('opengl', 'Graphics', 'OpenGL', bool)
+        self.setconf('max_fps', 'Graphics', 'Maximum FPS', float, 60.0)
+
+    def parse_control(self):
+        """Parse control configurations."""
+        self.key, self.mouse = {}, {}
+        for cmd, alias, bind in self.DEFAULT_BINDINGS:
+            i = self.getconf('Control', cmd, fallback=bind).lower()
+            if re.match('mouse[1-3]$', i):
+                if alias not in ('shot', 'slash'):
+                    raise ValueError(self.WEIRD_MOUSE_ERR.format(cmd))
+                self.mouse[alias] = int(i[-1]) - 1
+                continue
+            if len(i) == 1:
+                self.key[alias] = ord(i)
+                continue
+            try:
+                self.key[alias] = getattr(pygame, 'K_{}'.format(i.upper()))
+            except AttributeError:
+                raise ValueError(self.INVALID_CONTROL_ERR.format(cmd, i))
+
+    def read_args(self, arguments):
+        """Read and parse a ArgumentParser.Namespace."""
+        if arguments.size is not None: self.width, self.height = arguments.size
+        if arguments.opengl is not None: self.opengl = arguments.opengl
+        if arguments.max_fps is not None: self.max_fps = arguments.max_fps
 
 
 def main():
     """Start game and main loop."""
     # Read configuration file
-    config = ConfigParser()
-    conf = config.read(USER_CONFIG)
-    if not conf: conf = config.read(SITE_CONFIG)
-    conf = conf[0] if conf else ''
-
-    # Read graphics configurations
-    width = getconf(config, 'Graphics', 'Screen width', int, 640)
-    height = getconf(config, 'Graphics', 'Screen height', int, 480)
-    scrtype = RESIZABLE
-    if getconf(config, 'Graphics', 'OpenGL', bool):
-        scrtype |= OPENGL | DOUBLEBUF
-    fps = max_fps = getconf(config, 'Graphics', 'Maximum FPS', float, 60.0)
-
-    # Read control configurations
-    key, mouse = {}, {}
-    for cmd, bind in DEFAULT_BINDINGS.items():
-        i = getconf(config, 'Control', cmd, fallback=bind).lower()
-        if re.match('mouse[1-3]$', i):
-            if cmd not in ('Long-range attack', 'Close-range attack'):
-                print('File "{}", section Control'.format(conf))
-                print('\tOne does not simply {} using a mouse'.format(cmd))
-                quit()
-            mouse[cmd] = int(i[-1]) - 1
-            continue
-        if len(i) == 1:
-            key[cmd] = ord(i)
-            continue
-        try:
-            key[cmd] = getattr(pygame, 'K_{}'.format(i.upper()))
-        except AttributeError:
-            print('File "{}", section Control, option {}'.format(conf, cmd))
-            print('\t"{}" is not recognized as a valid input'.format(i))
-            quit()
+    dirs = AppDirs(appname='brutalmaze', appauthor=False, multipath=True)
+    parents = dirs.site_config_dir.split(pathsep)
+    parents.append(dirs.user_config_dir)
+    filenames = [join(parent, 'settings.ini') for parent in parents]
+    config = ConfigReader(filenames)
+    config.parse_graphics()
+
+    # Parse command line arguments
+    parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
+    parser.add_argument('-v', '--version', action='version',
+                        version='Brutal Maze {}'.format(__version__))
+    parser.add_argument(
+        '-c', '--config', metavar='PATH',
+        help='location of the configuration file (fallback: {})'.format(
+            pathsep.join(filenames)))
+    parser.add_argument(
+        '-s', '--size', type=int, nargs=2, metavar=('X', 'Y'),
+        help='the desired screen size (fallback: {}x{})'.format(config.width,
+                                                                config.height))
+    parser.add_argument(
+        '--opengl', action='store_true', default=None,
+        help='enable OpenGL (fallback: {})'.format(config.opengl))
+    parser.add_argument('--no-opengl', action='store_false', dest='opengl',
+                        help='disable OpenGL')
+    parser.add_argument(
+        '-f', '--max-fps', type=float, metavar='FPS',
+        help='the desired maximum FPS (fallback: {})'.format(config.max_fps))
+    args = parser.parse_args()
+
+    # Manipulate config
+    if args.config: config.config.read(args.config)
+    config.read_args(args)
+    config.parse_graphics()
+    config.parse_control()
 
     # Initialization
     pygame.mixer.pre_init(frequency=44100)
@@ -95,10 +168,12 @@ def main():
     pygame.mixer.music.play(-1)
     pygame.display.set_icon(ICON)
     pygame.fastevent.init()
-    maze = Maze((width, height), scrtype, fps)
-    clock, flash_time, going = pygame.time.Clock(), deque(), True
+    clock, flash_time, fps = pygame.time.Clock(), deque(), config.max_fps
+    scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE
+    maze = Maze((config.width, config.height), scrtype, fps)
 
     # Main loop
+    going = True
     while going:
         events = pygame.fastevent.get()
         for event in events:
@@ -107,24 +182,24 @@ def main():
             elif event.type == VIDEORESIZE:
                 maze.resize((event.w, event.h), scrtype)
             elif event.type == KEYDOWN:
-                if event.key == key['New game']:
+                if event.key == config.key['new']:
                     maze.__init__((maze.w, maze.h), scrtype, fps)
-                elif event.key == key['Pause'] and not maze.hero.dead:
+                elif event.key == config.key['pause'] and not maze.hero.dead:
                     maze.paused ^= True
 
         if not maze.hero.dead:
             keys = pygame.key.get_pressed()
-            maze.move(keys[key['Move left']] - keys[key['Move right']],
-                      keys[key['Move up']] - keys[key['Move down']], fps)
+            maze.move(keys[config.key['left']] - keys[config.key['right']],
+                      keys[config.key['up']] - keys[config.key['down']], fps)
             buttons = pygame.mouse.get_pressed()
             try:
-                maze.hero.firing = keys[key['Long-range attack']]
+                maze.hero.firing = keys[config.key['shot']]
             except KeyError:
-                maze.hero.firing = buttons[mouse['Long-range attack']]
+                maze.hero.firing = buttons[config.mouse['shot']]
             try:
-                maze.hero.slashing = keys[key['Close-range attack']]
+                maze.hero.slashing = keys[config.key['slash']]
             except KeyError:
-                maze.hero.slashing = buttons[mouse['Close-range attack']]
+                maze.hero.slashing = buttons[config.mouse['slash']]
 
         # Compare current FPS with the average of the last 5 seconds
         if len(flash_time) > 5:
@@ -132,7 +207,7 @@ def main():
             flash_time.popleft()
             if new_fps < fps:
                 fps -= 1
-            elif fps < max_fps and not maze.paused:
+            elif fps < config.max_fps and not maze.paused:
                 fps += 5
         maze.update(fps)
         flash_time.append(pygame.time.get_ticks())
diff --git a/setup.py b/setup.py
index 8a2421a..db966d1 100755
--- a/setup.py
+++ b/setup.py
@@ -7,8 +7,8 @@ with open('README.rst') as f:
 
 setup(
     name='brutalmaze',
-    version='0.5.0',
-    description='A hash and slash game with fast-paced action and a minimalist art style',
+    version='0.5.1',
+    description='A minimalist hack and slash game with fast-paced action',
     long_description=long_description,
     url='https://github.com/McSinyx/brutalmaze',
     author='Nguyễn Gia Phong',