Changeset 224

Show
Ignore:
Timestamp:
12/25/06 18:25:30 (5 years ago)
Author:
mg
Message:

Ability to customize keyboard controls.

Location:
trunk
Files:
4 modified

Legend:

Unmodified
Added
Removed
  • trunk/NEWS.txt

    r216 r224  
     1December 25, 2006: 
     2 
     3  - Keyboard controls can now be customized. 
     4 
    15February 20, 2006: Released version 0.9.2 (also known as the "a month of 
    26procrastination" release). 
  • trunk/src/pyspacewar/tests/test_ui.py

    r76 r224  
    217217 
    218218 
     219def setUp(test=None): 
     220    import pygame 
     221    pygame.init() # so that pygame.key.name() works 
     222    # unfortunately, on linux, if $DISPLAY is unset, pygame.init doesn't 
     223    # complain, but pygame.key.name() returns 'unknown key' for all keys 
     224 
     225 
    219226def test_suite(): 
    220227    path = os.path.join(os.path.dirname(__file__), os.path.pardir) 
     
    222229        sys.path.append(path) 
    223230    return unittest.TestSuite([ 
    224                         doctest.DocTestSuite('ui'), 
     231                        doctest.DocTestSuite('ui', setUp=setUp), 
    225232                        doctest.DocTestSuite()]) 
    226233 
  • trunk/src/pyspacewar/ui.py

    r223 r224  
    2727 
    2828 
     29DEFAULT_CONTROLS = { 
     30    # Player 1 
     31    'P1_TOGGLE_AI': K_1, 
     32    'P1_LEFT': K_LEFT, 
     33    'P1_RIGHT': K_RIGHT, 
     34    'P1_FORWARD': K_UP, 
     35    'P1_BACKWARD': K_DOWN, 
     36    'P1_BRAKE': K_RALT, 
     37    'P1_FIRE': K_RCTRL, 
     38    # Player 2 
     39    'P2_TOGGLE_AI': K_2, 
     40    'P2_LEFT': K_a, 
     41    'P2_RIGHT': K_d, 
     42    'P2_FORWARD': K_w, 
     43    'P2_BACKWARD': K_s, 
     44    'P2_BRAKE': K_LALT, 
     45    'P2_FIRE': K_LCTRL, 
     46} 
     47 
     48 
    2949HELP_TEXT = u"""\ 
    3050=PySpaceWar= 
     
    4161=Player 1 Controls= 
    4262 
    43   LEFT, RIGHT    \u2014 rotate 
    44   UP              \u2014 accelerate in the direction you're facing 
    45   DOWN            \u2014 accelerate in the opposite direction 
    46   RCTRL           \u2014 launch a missile 
    47   RALT            \u2014 brake (lose 5% speed) 
    48   1               \u2014 enable/disable computer control 
     63  P1_LEFT, P1_RIGHT \u2014 rotate 
     64  P1_FORWARD      \u2014 accelerate in the direction you're facing 
     65  P1_BACKWARD     \u2014 accelerate in the opposite direction 
     66  P1_FIRE         \u2014 launch a missile 
     67  P1_BRAKE        \u2014 brake (lose 5% speed) 
     68  P1_TOGGLE_AI    \u2014 enable/disable computer control 
    4969 
    5070=Player 2 Controls= 
    5171 
    52   A, D            \u2014 rotate 
    53   W               \u2014 accelerate in the direction you're facing 
    54   S               \u2014 accelerate in the opposite direction 
    55   LCTRL           \u2014 launch a missile 
    56   LALT            \u2014 brake (lose 5% speed) 
    57   2               \u2014 enable/disable computer control 
     72  P2_LEFT, P2_RIGHT \u2014 rotate 
     73  P2_FORWARD      \u2014 accelerate in the direction you're facing 
     74  P2_BACKWARD     \u2014 accelerate in the opposite direction 
     75  P2_FIRE         \u2014 launch a missile 
     76  P2_BRAKE        \u2014 brake (lose 5% speed) 
     77  P2_TOGGLE_AI    \u2014 enable/disable computer control 
    5878 
    5979=Other Controls= 
     
    84104version. 
    85105""" 
     106 
     107 
     108def key_name(key): 
     109    """Return the name of the key. 
     110 
     111        >>> key_name(K_RCTRL) 
     112        'RIGHT CTRL' 
     113        >>> key_name(None) 
     114        '(unset)' 
     115 
     116    """ 
     117    if not key: 
     118        return '(unset)' 
     119    return pygame.key.name(key).upper() 
     120 
     121 
     122def fixup_keys_in_text(text, controls): 
     123    """Replace action names with key names in help text. 
     124 
     125        >>> fixup_keys_in_text('Press FIRE to start', {'FIRE': K_RCTRL}) 
     126        'Press RIGHT CTRL to start' 
     127 
     128    """ 
     129    for action, key in controls.items(): 
     130        text = text.replace(action, key_name(key)) 
     131    return text 
    86132 
    87133 
     
    801847        self.top = 0 
    802848        self.item_height = item_height 
    803         self.surface = pygame.Surface((self.width, self.full_height)) 
    804         self.surface.set_alpha(255 * 0.9) 
    805         self.surface.set_colorkey((1, 1, 1)) 
    806         self.invalidate() 
     849        self.resize() 
    807850 
    808851    def position(self, surface, margin=10): 
     
    820863        return HUDElement.position(self, surface, margin) 
    821864 
     865    def resize(self): 
     866        self.surface = pygame.Surface((self.width, self.full_height)) 
     867        self.surface.set_alpha(255 * 0.9) 
     868        self.surface.set_colorkey((1, 1, 1)) 
     869        self.invalidate() 
     870 
    822871    def invalidate(self): 
    823872        """Indicate that the menu needs to be redrawn.""" 
     
    830879        for item in items: 
    831880            size = font.size(item) 
     881            if '\t' in item: 
     882                size = (size[0] + xpadding * 2, size[1]) 
    832883            width = max(width, size[0]) 
    833884            height = max(height, size[1]) 
     
    858909                fg_color = self.normal_fg_color 
    859910                bg_color = self.normal_bg_color 
    860             img = self.font.render(item, True, fg_color) 
    861911            self.surface.fill(bg_color, (x, y, self.width, self.item_height)) 
    862             self.surface.blit(img, 
    863                               (x + (self.width - img.get_width())/2, 
    864                                y + (self.item_height - img.get_height())/2)) 
     912            if '\t' in item: 
     913                # align left and right 
     914                parts = item.split('\t', 1) 
     915                img = self.font.render(parts[0], True, fg_color) 
     916                margin = (self.item_height - img.get_height())/2 
     917                self.surface.blit(img, (x + self.xpadding, y + margin)) 
     918                img = self.font.render(parts[1], True, fg_color) 
     919                self.surface.blit(img, 
     920                                  (x + self.width - img.get_width() 
     921                                     - self.xpadding, 
     922                                   y + margin)) 
     923            else: 
     924                # center 
     925                img = self.font.render(item, True, fg_color) 
     926                margin = (self.item_height - img.get_height())/2 
     927                self.surface.blit(img, 
     928                                  (x + (self.width - img.get_width())/2, 
     929                                   y + margin)) 
    865930            for ax in (0, self.width-1): 
    866931                for ay in (0, self.item_height-1): 
     
    875940        surface.blit(self.surface, (x, y), 
    876941                     (0, self.top, self.width, self.height)) 
     942 
     943 
     944class HUDControlsMenu(HUDMenu): 
     945    """A scrolling menu for keyboard controls.""" 
     946 
     947    def __init__(self, font, items, xalign=0.5, yalign=0.5, 
     948                 xpadding=8, ypadding=4, yspacing=2): 
     949        HUDMenu.__init__(self, font, items, xalign, yalign, xpadding, 
     950                         ypadding, yspacing) 
     951 
     952    def position(self, surface, margin=10): 
     953        """Calculate screen position for the widget.""" 
     954        width = surface.get_width() - 2 * margin - 2 * self.xpadding 
     955        if width != self.width: 
     956            self.width = width 
     957            self.resize() 
     958        return HUDMenu.position(self, surface, margin) 
    877959 
    878960 
     
    915997 
    916998 
     999class HUDMessage(HUDElement): 
     1000    """An message box.""" 
     1001 
     1002    fg_color = (220, 255, 255) 
     1003    bg_color = (24, 120, 14) 
     1004 
     1005    def __init__(self, font, text, xpadding=16, ypadding=16, xalign=0.5, 
     1006                 yalign=0.5): 
     1007        width, height = font.size(text) 
     1008        width += 2*xpadding 
     1009        height += 2*ypadding 
     1010        HUDElement.__init__(self, width, height, xalign, yalign) 
     1011        self.xpadding = xpadding 
     1012        self.ypadding = ypadding 
     1013        self.font = font 
     1014        self.text = text 
     1015        self.surface = pygame.Surface((self.width, self.height)) 
     1016        self.surface.set_alpha(255 * 0.9) 
     1017        self.surface.set_colorkey((1, 1, 1)) 
     1018        self.surface.fill(self.bg_color) 
     1019        img = self.font.render(text, True, self.fg_color) 
     1020        x = (self.width - img.get_width()) / 2 
     1021        y = (self.height - img.get_height()) / 2 
     1022        self.surface.blit(img, (x, y)) 
     1023        for dx, dy in (0, 0), (1, 0), (0, 1): 
     1024            self.surface.set_at((dx, dy), (1, 1, 1)) 
     1025            self.surface.set_at((self.width-1-dx, dy), (1, 1, 1)) 
     1026            self.surface.set_at((dx, self.height-1-dy), (1, 1, 1)) 
     1027            self.surface.set_at((self.width-1-dx, self.height-1-dy), (1, 1, 1)) 
     1028 
     1029    def draw(self, surface): 
     1030        """Draw the element.""" 
     1031        x, y = self.position(surface) 
     1032        surface.blit(self.surface, (x, y)) 
     1033 
     1034 
    9171035class UIMode(object): 
    9181036    """Mode of user interface. 
     
    9781096    def handle_key_press(self, event): 
    9791097        """Handle a KEYDOWN event.""" 
    980         handler_and_args = self._keymap_once.get(event.key) 
     1098        key = event.key 
     1099        if key in self.ui.rev_controls: 
     1100            action = self.ui.rev_controls[key] 
     1101            if action in self._keymap_once or action in self._keymap_repeat: 
     1102                key = action 
     1103        handler_and_args = self._keymap_once.get(key) 
    9811104        if handler_and_args: 
    9821105            handler, args = handler_and_args 
    9831106            handler(*args) 
    984         elif event.key not in self._keymap_repeat: 
     1107        elif key not in self._keymap_repeat: 
    9851108            self.handle_any_other_key(event) 
    9861109 
     
    9921115        """Handle any keys that are pressed.""" 
    9931116        for key, (handler, args) in self._keymap_repeat.items(): 
     1117            key = self.ui.controls.get(key, key) 
    9941118            if pressed[key]: 
    9951119                handler(*args) 
     
    10861210        """Initialize the mode.""" 
    10871211        self.init_menu() 
    1088         self.menu = HUDMenu(self.ui.menu_font, 
    1089                             [item[0] for item in self.menu_items]) 
     1212        self.menu = self.create_menu() 
     1213        if self.has_no_action(self.menu.selected_item): 
     1214            self.select_next_item() 
    10901215        self.on_key(K_UP, self.select_prev_item) 
    10911216        self.on_key(K_DOWN, self.select_next_item) 
     
    11081233        ] 
    11091234 
     1235    def create_menu(self): 
     1236        """Create the menu control for display.""" 
     1237        return HUDMenu(self.ui.menu_font, 
     1238                            [item[0] for item in self.menu_items]) 
     1239 
     1240    def has_no_action(self, item_idx): 
     1241        """Is this menu item just an unselectable label?""" 
     1242        return len(self.menu_items[item_idx]) == 1 
     1243 
    11101244    def reinit_menu(self): 
    11111245        """Reinitialize the menu.""" 
     
    11181252        """Select menu item under cursor.""" 
    11191253        which = self.menu.find(self.ui.screen, pos) 
    1120         if which != -1: 
     1254        if which != -1 and not self.has_no_action(which): 
    11211255            self.menu.selected_item = which 
    11221256        return which 
     
    11541288            self.menu.selected_item = len(self.menu.items) 
    11551289        self.menu.selected_item -= 1 
     1290        if self.has_no_action(self.menu.selected_item): 
     1291            self.select_prev_item() 
    11561292 
    11571293    def select_next_item(self): 
     
    11601296        if self.menu.selected_item == len(self.menu.items): 
    11611297            self.menu.selected_item = 0 
     1298            self.menu.top = 0 
     1299        if self.has_no_action(self.menu.selected_item): 
     1300            self.select_next_item() 
    11621301 
    11631302    def activate_item(self): 
    11641303        """Activate the selected menu item.""" 
    11651304        action = self.menu_items[self.menu.selected_item][1:] 
    1166         handler = action[0] 
    1167         args = action[1:] 
    1168         handler(*args) 
     1305        if action: 
     1306            handler = action[0] 
     1307            args = action[1:] 
     1308            handler(*args) 
    11691309 
    11701310    def close_menu(self): 
     
    12181358                                          or 'Show missile orbits', 
    12191359             self.toggle_missile_orbits), 
     1360            ('Controls', self.ui.controls_menu), 
    12201361            ('Return to main menu', self.close_menu), 
    12211362        ] 
     
    12631404        self.ui.switch_to_mode(mode) 
    12641405        self.reinit_menu() 
     1406 
     1407 
     1408class ControlsMenuMode(MenuMode): 
     1409    """Mode: controls menu.""" 
     1410 
     1411    def items(self, label, items): 
     1412        return ([(label, )] + 
     1413                [(title + '\t' + key_name(self.ui.controls[action]), 
     1414                  self.set_control, title, action) 
     1415                 for title, action in items]) 
     1416 
     1417    def init_menu(self): 
     1418        self.menu_items = self.items('Player 1', [ 
     1419                ('Turn left', 'P1_LEFT'), 
     1420                ('Turn right', 'P1_RIGHT'), 
     1421                ('Accelerate', 'P1_FORWARD'), 
     1422                ('Decelerate', 'P1_BACKWARD'), 
     1423                ('Launch missile', 'P1_FIRE'), 
     1424                ('Brake', 'P1_BRAKE'), 
     1425                ('Toggle computer control', 'P1_TOGGLE_AI'), 
     1426        ]) + self.items('Player 2', [ 
     1427                ('Turn left', 'P2_LEFT'), 
     1428                ('Turn right', 'P2_RIGHT'), 
     1429                ('Accelerate', 'P2_FORWARD'), 
     1430                ('Decelerate', 'P2_BACKWARD'), 
     1431                ('Launch missile', 'P2_FIRE'), 
     1432                ('Brake', 'P2_BRAKE'), 
     1433                ('Toggle computer control', 'P2_TOGGLE_AI'), 
     1434        ]) + [ 
     1435            ('Return to options menu', self.close_menu), 
     1436        ] 
     1437 
     1438    def create_menu(self): 
     1439        """Create the menu control for display.""" 
     1440        return HUDControlsMenu(self.ui.input_font, 
     1441                               [item[0] for item in self.menu_items]) 
     1442 
     1443    def set_control(self, action, key): 
     1444        """Change a control""" 
     1445        self.ui.ui_mode = WaitingForControlMode(self.ui, action, key) 
     1446 
     1447 
     1448class WaitingForControlMode(UIMode): 
     1449    """Mode: controls menu, waiting for a key press.""" 
     1450 
     1451    inherit_pause_from_prev_mode = True 
     1452 
     1453    def __init__(self, ui, action, key): 
     1454        self.action = action 
     1455        self.key = key 
     1456        UIMode.__init__(self, ui) 
     1457 
     1458    def init(self): 
     1459        self.prompt = HUDMessage(self.ui.menu_font, 
     1460                                 "Press a key or ESC to cancel") 
     1461        self.on_key(K_PAUSE, self.ui.pause) 
     1462        self.on_key(K_ESCAPE, self.return_to_previous_mode) 
     1463 
     1464    def draw(self, screen): 
     1465        """Draw extra things pertaining to the mode.""" 
     1466        self.prev_mode.draw(screen) 
     1467        self.prompt.draw(screen) 
     1468 
     1469    def handle_any_other_key(self, event): 
     1470        """Handle a KEYDOWN event for unknown keys.""" 
     1471        self.ui.set_control(self.key, event.key) 
     1472        self.prev_mode.reinit_menu() 
     1473        self.return_to_previous_mode() 
     1474 
     1475    def handle_mouse_release(self, event): 
     1476        """Handle a MOUSEBUTTONUP event.""" 
     1477        self.return_to_previous_mode() 
    12651478 
    12661479 
     
    12961509        self.while_key(K_MINUS, self.ui.zoom_out) 
    12971510        # Player 1 
    1298         self.on_key(K_1, self.ui.toggle_ai, 0) 
    1299         self.while_key(K_LEFT, self.ui.turn_left, 0) 
    1300         self.while_key(K_RIGHT, self.ui.turn_right, 0) 
    1301         self.while_key(K_UP, self.ui.accelerate, 0) 
    1302         self.while_key(K_DOWN, self.ui.backwards, 0) 
    1303         self.while_key(K_RALT, self.ui.brake, 0) 
    1304         self.on_key(K_RCTRL, self.ui.launch_missile, 0) 
     1511        self.on_key('P1_TOGGLE_AI', self.ui.toggle_ai, 0) 
     1512        self.while_key('P1_LEFT', self.ui.turn_left, 0) 
     1513        self.while_key('P1_RIGHT', self.ui.turn_right, 0) 
     1514        self.while_key('P1_FORWARD', self.ui.accelerate, 0) 
     1515        self.while_key('P1_BACKWARD', self.ui.backwards, 0) 
     1516        self.while_key('P1_BRAKE', self.ui.brake, 0) 
     1517        self.on_key('P1_FIRE', self.ui.launch_missile, 0) 
    13051518        # Player 2 
    1306         self.on_key(K_2, self.ui.toggle_ai, 1) 
    1307         self.while_key(K_a, self.ui.turn_left, 1) 
    1308         self.while_key(K_d, self.ui.turn_right, 1) 
    1309         self.while_key(K_w, self.ui.accelerate, 1) 
    1310         self.while_key(K_s, self.ui.backwards, 1) 
    1311         self.while_key(K_LALT, self.ui.brake, 1) 
    1312         self.on_key(K_LCTRL, self.ui.launch_missile, 1) 
     1519        self.on_key('P2_TOGGLE_AI', self.ui.toggle_ai, 1) 
     1520        self.while_key('P2_LEFT', self.ui.turn_left, 1) 
     1521        self.while_key('P2_RIGHT', self.ui.turn_right, 1) 
     1522        self.while_key('P2_FORWARD', self.ui.accelerate, 1) 
     1523        self.while_key('P2_BACKWARD', self.ui.backwards, 1) 
     1524        self.while_key('P2_BRAKE', self.ui.brake, 1) 
     1525        self.on_key('P2_FIRE', self.ui.launch_missile, 1) 
    13131526 
    13141527    def handle_mouse_release(self, event): 
     
    14051618    mouse_visible = True 
    14061619 
    1407     def draw(self, screen): 
    1408         """Draw extra things pertaining to the mode.""" 
    1409         self.help_text.draw(screen) 
    1410  
    14111620    def init(self): 
    14121621        """Initialize the mode.""" 
     
    14201629        self.help_text = HUDFormattedText(self.ui.help_font, 
    14211630                                          self.ui.help_bold_font, 
    1422                                           HELP_TEXT, 
     1631                                          fixup_keys_in_text(HELP_TEXT, 
     1632                                                             self.ui.controls), 
    14231633                                          small_font=self.ui.hud_font) 
     1634 
     1635    def draw(self, screen): 
     1636        """Draw extra things pertaining to the mode.""" 
     1637        self.help_text.draw(screen) 
    14241638 
    14251639    def handle_mouse_release(self, event): 
     
    14721686    def __init__(self): 
    14731687        self.rng = random.Random() 
     1688        self.controls = dict(DEFAULT_CONTROLS) 
     1689        self.rev_controls = dict([(value, key) 
     1690                                  for (key, value) in self.controls.items()]) 
     1691        assert len(self.controls) == len(self.rev_controls) 
    14741692 
    14751693    def init(self): 
     
    17521970        self.ui_mode = ScreenResolutionMenuMode(self) 
    17531971 
     1972    def controls_menu(self): 
     1973        """Enter the controls menu.""" 
     1974        self.ui_mode = ControlsMenuMode(self) 
     1975 
    17541976    def watch_demo(self): 
    17551977        """Go back to demo mode.""" 
     
    17972019        self.fullscreen_mode = mode 
    17982020        self._set_display_mode() 
     2021 
     2022    def set_control(self, action, key): 
     2023        """Change a key mapping""" 
     2024        if key in self.rev_controls: 
     2025            old_action = self.rev_controls[key] 
     2026            self.controls[old_action] = None 
     2027        self.controls[action] = key 
     2028        self.rev_controls[key] = action 
    17992029 
    18002030    def zoom_in(self): 
  • trunk/src/pyspacewar/version.py

    r219 r224  
    1212    # This is slightly misleading: svn_revision contains the last changed 
    1313    # revision number for version.py, not the revision number of the whole 
    14     # repository. 
     14    # repository.  Using os.popen('svnversion').read() would be better, but 
     15    # only if the end-user has subversion installed.