├── .gitignore ├── config.py ├── game ├── __init__.py ├── core.py └── radio.py ├── images ├── border.png ├── map_icons │ ├── camp.png │ ├── cave.png │ ├── city.png │ ├── factory.png │ ├── landmark.png │ ├── metro.png │ ├── misc.png │ ├── monument.png │ ├── office.png │ ├── ruin.png │ ├── settlement.png │ ├── sewer.png │ └── vault.png ├── overlay.png └── pipboy.png ├── main.py ├── monofonto.ttf ├── pypboy ├── __init__.py ├── core.py ├── data.py ├── modules │ ├── __init__.py │ ├── data │ │ ├── __init__.py │ │ ├── entities.py │ │ ├── local_map.py │ │ ├── misc.py │ │ ├── quests.py │ │ ├── radio.py │ │ └── world_map.py │ ├── items │ │ ├── __init__.py │ │ ├── aid.py │ │ ├── ammo.py │ │ ├── apparel.py │ │ ├── misc.py │ │ └── weapons.py │ └── stats │ │ ├── __init__.py │ │ ├── general.py │ │ ├── perks.py │ │ ├── skills.py │ │ ├── special.py │ │ └── status.py └── ui.py ├── requirements.txt └── sounds ├── dial_move.ogg ├── module_change.ogg └── submodule_change.ogg /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .project 3 | .pydevproject 4 | .settings 5 | lib 6 | radio -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | WIDTH = 480 4 | HEIGHT = 360 5 | 6 | # OUTPUT_WIDTH = 480 7 | # OUTPUT_HEIGHT = 360 8 | 9 | MAP_FOCUS = (-5.9347681, 54.5889076) 10 | 11 | EVENTS = { 12 | 'SONG_END': pygame.USEREVENT + 1 13 | } 14 | 15 | ACTIONS = { 16 | pygame.K_F1: "module_stats", 17 | pygame.K_F2: "module_items", 18 | pygame.K_F3: "module_data", 19 | pygame.K_1: "knob_1", 20 | pygame.K_2: "knob_2", 21 | pygame.K_3: "knob_3", 22 | pygame.K_4: "knob_4", 23 | pygame.K_5: "knob_5", 24 | pygame.K_UP: "dial_up", 25 | pygame.K_DOWN: "dial_down" 26 | } 27 | 28 | # Using GPIO.BOARD as mode 29 | GPIO_ACTIONS = { 30 | 22: "module_stats", 31 | 24: "module_items", 32 | 26: "module_data", 33 | 13: "knob_1", 34 | 11: "knob_2", 35 | 7: "knob_3", 36 | 5: "knob_4", 37 | 3: "knob_5", 38 | # 8: "dial_up", 39 | # 7: "dial_down" 40 | } 41 | 42 | 43 | MAP_ICONS = { 44 | "camp": pygame.image.load('images/map_icons/camp.png'), 45 | "factory": pygame.image.load('images/map_icons/factory.png'), 46 | "metro": pygame.image.load('images/map_icons/metro.png'), 47 | "misc": pygame.image.load('images/map_icons/misc.png'), 48 | "monument": pygame.image.load('images/map_icons/monument.png'), 49 | "vault": pygame.image.load('images/map_icons/vault.png'), 50 | "settlement": pygame.image.load('images/map_icons/settlement.png'), 51 | "ruin": pygame.image.load('images/map_icons/ruin.png'), 52 | "cave": pygame.image.load('images/map_icons/cave.png'), 53 | "landmark": pygame.image.load('images/map_icons/landmark.png'), 54 | "city": pygame.image.load('images/map_icons/city.png'), 55 | "office": pygame.image.load('images/map_icons/office.png'), 56 | "sewer": pygame.image.load('images/map_icons/sewer.png'), 57 | } 58 | 59 | AMENITIES = { 60 | 'pub': MAP_ICONS['vault'], 61 | 'nightclub': MAP_ICONS['vault'], 62 | 'bar': MAP_ICONS['vault'], 63 | 'fast_food': MAP_ICONS['sewer'], 64 | 'cafe': MAP_ICONS['sewer'], 65 | 'drinking_water': MAP_ICONS['sewer'], 66 | 'restaurant': MAP_ICONS['settlement'], 67 | 'cinema': MAP_ICONS['office'], 68 | 'pharmacy': MAP_ICONS['office'], 69 | 'school': MAP_ICONS['office'], 70 | 'bank': MAP_ICONS['monument'], 71 | 'townhall': MAP_ICONS['monument'], 72 | 'bicycle_parking': MAP_ICONS['misc'], 73 | 'place_of_worship': MAP_ICONS['misc'], 74 | 'theatre': MAP_ICONS['misc'], 75 | 'bus_station': MAP_ICONS['misc'], 76 | 'parking': MAP_ICONS['misc'], 77 | 'fountain': MAP_ICONS['misc'], 78 | 'marketplace': MAP_ICONS['misc'], 79 | 'atm': MAP_ICONS['misc'], 80 | } 81 | 82 | pygame.font.init() 83 | FONTS = {} 84 | for x in range(10, 28): 85 | FONTS[x] = pygame.font.Font('monofonto.ttf', x) -------------------------------------------------------------------------------- /game/__init__.py: -------------------------------------------------------------------------------- 1 | from core import Entity 2 | from core import EntityGroup 3 | from core import Engine 4 | -------------------------------------------------------------------------------- /game/core.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import time 3 | 4 | class Engine(object): 5 | 6 | EVENTS_UPDATE = pygame.USEREVENT + 1 7 | EVENTS_RENDER = pygame.USEREVENT + 2 8 | 9 | def __init__(self, title, width, height, *args, **kwargs): 10 | super(Engine, self).__init__(*args, **kwargs) 11 | self.window = pygame.display.set_mode((width, height)) 12 | self.screen = pygame.display.get_surface() 13 | pygame.display.set_caption(title) 14 | pygame.mouse.set_visible(False) 15 | 16 | self.groups = [] 17 | self.root_children = EntityGroup() 18 | self.background = pygame.surface.Surface(self.screen.get_size()).convert() 19 | self.background.fill((0, 0, 0)) 20 | 21 | self.rescale = False 22 | self.last_render_time = 0 23 | 24 | def render(self): 25 | if self.last_render_time == 0: 26 | self.last_render_time = time.time() 27 | return 28 | else: 29 | interval = time.time() - self.last_render_time 30 | self.last_render_time = time.time() 31 | self.root_children.clear(self.screen, self.background) 32 | self.root_children.render(interval) 33 | self.root_children.draw(self.screen) 34 | for group in self.groups: 35 | group.render(interval) 36 | group.draw(self.screen) 37 | pygame.display.flip() 38 | return interval 39 | 40 | def update(self): 41 | self.root_children.update() 42 | for group in self.groups: 43 | group.update() 44 | 45 | def add(self, group): 46 | if group not in self.groups: 47 | self.groups.append(group) 48 | 49 | def remove(self, group): 50 | if group in self.groups: 51 | self.groups.remove(group) 52 | 53 | 54 | class EntityGroup(pygame.sprite.LayeredDirty): 55 | def render(self, interval): 56 | for entity in self: 57 | entity.render(interval) 58 | 59 | def move(self, x, y): 60 | for child in self: 61 | child.rect.move(x, y) 62 | 63 | 64 | class Entity(pygame.sprite.DirtySprite): 65 | def __init__(self, dimensions=(0, 0), layer=0, *args, **kwargs): 66 | super(Entity, self).__init__(*args, **kwargs) 67 | self.image = pygame.surface.Surface(dimensions) 68 | self.rect = self.image.get_rect() 69 | self.image = self.image.convert() 70 | self.groups = pygame.sprite.LayeredDirty() 71 | self.layer = layer 72 | self.dirty = 2 73 | self.blendmode = pygame.BLEND_RGBA_ADD 74 | 75 | def render(self, interval=0, *args, **kwargs): 76 | pass 77 | 78 | def update(self, *args, **kwargs): 79 | pass 80 | -------------------------------------------------------------------------------- /game/radio.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | import game 4 | import os 5 | import pygame 6 | import traceback 7 | import numpy 8 | from numpy.fft import fft 9 | from math import log10 10 | import math 11 | from random import randint 12 | import copy 13 | import game.globals as globals 14 | 15 | 16 | class Radio(game.Entity): 17 | def __init__(self): 18 | super(Radio, self).__init__((globals.WIDTH, globals.HEIGHT)) 19 | # set up the mixer 20 | 21 | try: pygame.mixer.quit() 22 | except: pass 23 | 24 | freq = 44100 # audio CD quality 25 | bitsize = -16 # unsigned 16 bit 26 | channels = 2 # 1 is mono, 2 is stereo 27 | buffer = 2048 # number of samples (experiment to get right sound) 28 | pygame.mixer.init(freq, bitsize, channels, buffer) 29 | self.osc = Oscilloscope() 30 | self.osc.open(self) 31 | self.paused = True 32 | self.loaded = False 33 | self.spectrum = None 34 | self.filename = "" 35 | 36 | def play_rnd(self): 37 | files = load_files() 38 | file = files[randint(0,len(files)-1)] 39 | self.filename = file 40 | pygame.mixer.music.load(file) 41 | self.spectrum = LogSpectrum(file,force_mono=True) 42 | pygame.mixer.music.play() 43 | self.loaded = True 44 | self.paused = False 45 | 46 | def play(self): 47 | if self.loaded: 48 | self.paused = False 49 | pygame.mixer.music.unpause() 50 | else: 51 | self.play_rnd() 52 | 53 | def stop(self): 54 | self.paused = True 55 | pygame.mixer.music.pause() 56 | 57 | def update(self, *args, **kwargs): 58 | super(Radio, self).update(*args, **kwargs) 59 | 60 | def render(self, *args, **kwargs): 61 | if not self.paused : 62 | f,p = None,[0 for i in range(21)] 63 | start = pygame.mixer.music.get_pos() / 1000.0 64 | try: 65 | f,p = self.spectrum.get_mono(start-0.001, start+0.001) 66 | except: 67 | pass 68 | self.osc.update(start*50,f,p) 69 | if self.osc: 70 | self.blit(self.osc.screen, (550, 150)) 71 | 72 | selectFont = pygame.font.Font('monofonto.ttf', 24) 73 | basicFont = pygame.font.Font('monofonto.ttf', 22) 74 | text = selectFont.render(" - Random Play Radio ", True, (105, 251, 187), (0, 0, 0)) 75 | self.blit(text, (75, 75)) 76 | text = basicFont.render(" 'r' selects a random song ", True, (105, 251, 187), (0, 0, 0)) 77 | self.blit(text, (75, 100)) 78 | text = basicFont.render(" 'p' to play 's' to stop ", True, (105, 251, 187), (0, 0, 0)) 79 | self.blit(text, (75, 120)) 80 | 81 | if self.filename: 82 | text = selectFont.render(u" %s " % self.filename[self.filename.rfind(os.sep)+1:], True, (105, 251, 187), (0, 0, 0)) 83 | self.blit(text, (75, 200)) 84 | 85 | super(Radio, self).update(*args, **kwargs) 86 | 87 | class Oscilloscope: 88 | 89 | def __init__(self): 90 | # Constants 91 | self.WIDTH, self.HEIGHT = 210, 200 92 | self.TRACE, self.AFTER, self.GREY = (80, 255, 100),(20, 155, 40),(20, 110, 30) 93 | self.embedded = False 94 | 95 | def open(self, screen=None): 96 | # Open window 97 | pygame.init() 98 | if screen: 99 | '''Embedded''' 100 | self.screen = pygame.Surface((self.WIDTH, self.HEIGHT), 0) 101 | self.embedded = True 102 | else: 103 | '''Own Display''' 104 | self.screen = pygame.display.set_mode((self.WIDTH, self.HEIGHT), 0) 105 | 106 | # Create a blank chart with vertical ticks, etc 107 | self.blank = numpy.zeros((self.WIDTH, self.HEIGHT, 3)) 108 | # Draw x-axis 109 | self.xaxis = self.HEIGHT/2 110 | self.blank[::, self.xaxis] = self.GREY 111 | self.blank[::, self.HEIGHT - 2] = self.TRACE 112 | self.blank[::, self.HEIGHT - 1] = self.TRACE 113 | self.blank[::50, self.HEIGHT - 4] = self.TRACE 114 | self.blank[::50, self.HEIGHT - 3] = self.TRACE 115 | self.blank[self.WIDTH - 2, ::] = self.TRACE 116 | self.blank[self.WIDTH - 1, ::] = self.TRACE 117 | self.blank[self.WIDTH - 3, ::40] = self.TRACE 118 | self.blank[self.WIDTH - 4, ::40] = self.TRACE 119 | 120 | # Draw vertical ticks 121 | vticks = [-80, -40, +40, +80] 122 | for vtick in vticks: self.blank[::5, self.xaxis + vtick] = self.GREY # Horizontals 123 | for vtick in vticks: self.blank[::50, ::5] = self.GREY # Verticals 124 | 125 | # Draw the 'blank' screen. 126 | pygame.surfarray.blit_array(self.screen, self.blank) # Blit the screen buffer 127 | pygame.display.flip() # Flip the double buffer 128 | 129 | 130 | def update(self,time,frequency,power): 131 | try: 132 | pixels = copy.copy(self.blank) 133 | offset = 1 134 | for x in range(self.WIDTH): 135 | offset = offset - 1 136 | if offset < -1: 137 | offset = offset + 1.1 138 | try: 139 | pow = power[int(x/10)] 140 | log = math.log10( pow ) 141 | offset = ((pow / math.pow(10, math.floor(log))) + log)*1.8 142 | except: 143 | pass 144 | try: 145 | y = float(self.xaxis) - (math.sin((float(x)+float(time))/5.0)*2.0*offset) 146 | pixels[x][y] = self.TRACE 147 | pixels[x][y-1] = self.AFTER 148 | pixels[x][y+1] = self.AFTER 149 | if abs(y) > 120: 150 | pixels[x][y-2] = self.AFTER 151 | pixels[x][y+2] = self.AFTER 152 | except: 153 | pass 154 | pygame.surfarray.blit_array(self.screen, pixels) # Blit the screen buffer 155 | if not self.embedded: 156 | pygame.display.flip() 157 | except Exception,e: 158 | print traceback.format_exc() 159 | 160 | def play_pygame(file): 161 | 162 | clock = pygame.time.Clock() 163 | # set up the mixer 164 | freq = 44100 # audio CD quality 165 | bitsize = -16 # unsigned 16 bit 166 | channels = 2 # 1 is mono, 2 is stereo 167 | buffer = 2048 # number of samples (experiment to get right sound) 168 | pygame.mixer.init(freq, bitsize, channels, buffer) 169 | 170 | while not pygame.mixer.get_init(): 171 | clock.tick(50) 172 | 173 | pygame.mixer.music.load(file) 174 | s = LogSpectrum(file,force_mono=True) 175 | osc = Oscilloscope() 176 | osc.open() 177 | 178 | f = None 179 | p = None 180 | running = True 181 | paused = False 182 | pygame.mixer.music.play() 183 | 184 | while pygame.mixer.music.get_busy() and running : 185 | if not paused: 186 | start = pygame.mixer.music.get_pos() / 1000.0 187 | try: 188 | f,p = s.get_mono(start-0.001, start+0.001) 189 | except: 190 | pass 191 | osc.update(start*50,f,p) 192 | pygame.time.wait(50) 193 | 194 | for event in pygame.event.get(): 195 | if (event.type == pygame.KEYUP) or (event.type == pygame.KEYDOWN): 196 | if (event.key == pygame.K_UP): 197 | pygame.mixer.music.pause() 198 | paused = True 199 | elif (event.key == pygame.K_DOWN): 200 | pygame.mixer.music.unpause() 201 | paused = False 202 | elif event.type == pygame.QUIT: 203 | running = False 204 | pygame.mixer.quit() 205 | 206 | if __name__ == "__main__": 207 | try: 208 | files = load_files() 209 | if files: 210 | play_pygame(files[randint(0,len(files)-1)]) 211 | except Exception, e: 212 | print traceback.format_exc() 213 | -------------------------------------------------------------------------------- /images/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/border.png -------------------------------------------------------------------------------- /images/map_icons/camp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/camp.png -------------------------------------------------------------------------------- /images/map_icons/cave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/cave.png -------------------------------------------------------------------------------- /images/map_icons/city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/city.png -------------------------------------------------------------------------------- /images/map_icons/factory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/factory.png -------------------------------------------------------------------------------- /images/map_icons/landmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/landmark.png -------------------------------------------------------------------------------- /images/map_icons/metro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/metro.png -------------------------------------------------------------------------------- /images/map_icons/misc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/misc.png -------------------------------------------------------------------------------- /images/map_icons/monument.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/monument.png -------------------------------------------------------------------------------- /images/map_icons/office.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/office.png -------------------------------------------------------------------------------- /images/map_icons/ruin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/ruin.png -------------------------------------------------------------------------------- /images/map_icons/settlement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/settlement.png -------------------------------------------------------------------------------- /images/map_icons/sewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/sewer.png -------------------------------------------------------------------------------- /images/map_icons/vault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/map_icons/vault.png -------------------------------------------------------------------------------- /images/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/overlay.png -------------------------------------------------------------------------------- /images/pipboy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/images/pipboy.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import config 3 | 4 | try: 5 | import RPi.GPIO as GPIO 6 | GPIO.setmode(GPIO.BOARD) 7 | config.GPIO_AVAILABLE = True 8 | except Exception, e: 9 | print "GPIO UNAVAILABLE (%s)" % e 10 | config.GPIO_AVAILABLE = False 11 | 12 | from pypboy.core import Pypboy 13 | 14 | try: 15 | pygame.mixer.init(44100, -16, 2, 2048) 16 | config.SOUND_ENABLED = True 17 | except: 18 | config.SOUND_ENABLED = False 19 | 20 | if __name__ == "__main__": 21 | boy = Pypboy('Pip-Boy 3000', config.WIDTH, config.HEIGHT) 22 | boy.run() 23 | -------------------------------------------------------------------------------- /monofonto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/monofonto.ttf -------------------------------------------------------------------------------- /pypboy/__init__.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import game 3 | import config 4 | import pypboy.ui 5 | 6 | if config.GPIO_AVAILABLE: 7 | import RPi.GPIO as GPIO 8 | 9 | 10 | class BaseModule(game.EntityGroup): 11 | 12 | submodules = [] 13 | 14 | def __init__(self, boy, *args, **kwargs): 15 | super(BaseModule, self).__init__() 16 | 17 | if config.GPIO_AVAILABLE: 18 | GPIO.setup(self.GPIO_LED_ID, GPIO.OUT) 19 | GPIO.output(self.GPIO_LED_ID, False) 20 | 21 | self.pypboy = boy 22 | self.position = (0, 40) 23 | 24 | self.footer = pypboy.ui.Footer() 25 | self.footer.menu = [] 26 | for mod in self.submodules: 27 | self.footer.menu.append(mod.label) 28 | self.footer.selected = self.footer.menu[0] 29 | self.footer.position = (0, config.HEIGHT - 80) 30 | self.add(self.footer) 31 | 32 | self.switch_submodule(0) 33 | 34 | self.action_handlers = { 35 | "pause": self.handle_pause, 36 | "resume": self.handle_resume 37 | } 38 | if config.SOUND_ENABLED: 39 | self.module_change_sfx = pygame.mixer.Sound('sounds/module_change.ogg') 40 | 41 | def move(self, x, y): 42 | super(BaseModule, self).move(x, y) 43 | if hasattr(self, 'active'): 44 | self.active.move(x, y) 45 | 46 | def switch_submodule(self, module): 47 | if hasattr(self, 'active') and self.active: 48 | self.active.handle_action("pause") 49 | self.remove(self.active) 50 | if len(self.submodules) > module: 51 | self.active = self.submodules[module] 52 | self.active.parent = self 53 | self.active.handle_action("resume") 54 | self.footer.select(self.footer.menu[module]) 55 | self.add(self.active) 56 | else: 57 | print "No submodule at %d" % module 58 | 59 | def render(self, interval): 60 | self.active.render(interval) 61 | super(BaseModule, self).render(interval) 62 | 63 | def handle_action(self, action, value=0): 64 | if action.startswith("knob_"): 65 | num = int(action[-1]) 66 | self.switch_submodule(num - 1) 67 | elif action in self.action_handlers: 68 | self.action_handlers[action]() 69 | else: 70 | if hasattr(self, 'active') and self.active: 71 | self.active.handle_action(action, value) 72 | 73 | def handle_event(self, event): 74 | if hasattr(self, 'active') and self.active: 75 | self.active.handle_event(event) 76 | 77 | 78 | def handle_pause(self): 79 | self.paused = True 80 | if config.GPIO_AVAILABLE: 81 | GPIO.output(self.GPIO_LED_ID, False) 82 | 83 | def handle_resume(self): 84 | self.paused = False 85 | if config.GPIO_AVAILABLE: 86 | GPIO.output(self.GPIO_LED_ID, True) 87 | if config.SOUND_ENABLED: 88 | self.module_change_sfx.play() 89 | 90 | 91 | class SubModule(game.EntityGroup): 92 | 93 | def __init__(self, parent, *args, **kwargs): 94 | super(SubModule, self).__init__() 95 | self.parent = parent 96 | 97 | self.action_handlers = { 98 | "pause": self.handle_pause, 99 | "resume": self.handle_resume 100 | } 101 | 102 | if config.SOUND_ENABLED: 103 | self.submodule_change_sfx = pygame.mixer.Sound('sounds/submodule_change.ogg') 104 | 105 | def handle_action(self, action, value=0): 106 | if action.startswith("dial_"): 107 | if hasattr(self, "menu"): 108 | self.menu.handle_action(action) 109 | elif action in self.action_handlers: 110 | self.action_handlers[action]() 111 | 112 | def handle_event(self, event): 113 | pass 114 | 115 | def handle_pause(self): 116 | self.paused = True 117 | 118 | def handle_resume(self): 119 | self.paused = False 120 | if config.SOUND_ENABLED: 121 | self.submodule_change_sfx.play() 122 | -------------------------------------------------------------------------------- /pypboy/core.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import config 3 | import game 4 | import pypboy.ui 5 | 6 | from pypboy.modules import data 7 | from pypboy.modules import items 8 | from pypboy.modules import stats 9 | 10 | if config.GPIO_AVAILABLE: 11 | import RPi.GPIO as GPIO 12 | 13 | 14 | class Pypboy(game.core.Engine): 15 | 16 | def __init__(self, *args, **kwargs): 17 | if hasattr(config, 'OUTPUT_WIDTH') and hasattr(config, 'OUTPUT_HEIGHT'): 18 | self.rescale = True 19 | super(Pypboy, self).__init__(*args, **kwargs) 20 | self.init_children() 21 | self.init_modules() 22 | 23 | self.gpio_actions = {} 24 | if config.GPIO_AVAILABLE: 25 | self.init_gpio_controls() 26 | 27 | def init_children(self): 28 | self.background = pygame.image.load('images/overlay.png') 29 | # border = pypboy.ui.Border() 30 | # self.root_children.add(border) 31 | self.header = pypboy.ui.Header() 32 | self.root_children.add(self.header) 33 | scanlines = pypboy.ui.Scanlines(800, 480, 3, 1, [(0, 13, 3, 50), (6, 42, 22, 100), (0, 13, 3, 50)]) 34 | self.root_children.add(scanlines) 35 | scanlines2 = pypboy.ui.Scanlines(800, 480, 8, 40, [(0, 10, 1, 0), (21, 62, 42, 90), (61, 122, 82, 100), (21, 62, 42, 90)] + [(0, 10, 1, 0) for x in range(50)], True) 36 | self.root_children.add(scanlines2) 37 | 38 | def init_modules(self): 39 | self.modules = { 40 | "data": data.Module(self), 41 | "items": items.Module(self), 42 | "stats": stats.Module(self) 43 | } 44 | for module in self.modules.values(): 45 | module.move(4, 40) 46 | self.switch_module("stats") 47 | 48 | def init_gpio_controls(self): 49 | for pin in config.GPIO_ACTIONS.keys(): 50 | print "Intialising pin %s as action '%s'" % (pin, config.GPIO_ACTIONS[pin]) 51 | GPIO.setup(pin, GPIO.IN) 52 | self.gpio_actions[pin] = config.GPIO_ACTIONS[pin] 53 | 54 | def check_gpio_input(self): 55 | for pin in self.gpio_actions.keys(): 56 | if not GPIO.input(pin): 57 | self.handle_action(self.gpio_actions[pin]) 58 | 59 | def update(self): 60 | if hasattr(self, 'active'): 61 | self.active.update() 62 | super(Pypboy, self).update() 63 | 64 | def render(self): 65 | interval = super(Pypboy, self).render() 66 | if hasattr(self, 'active'): 67 | self.active.render(interval) 68 | 69 | def switch_module(self, module): 70 | if module in self.modules: 71 | if hasattr(self, 'active'): 72 | self.active.handle_action("pause") 73 | self.remove(self.active) 74 | self.active = self.modules[module] 75 | self.active.parent = self 76 | self.active.handle_action("resume") 77 | self.add(self.active) 78 | else: 79 | print "Module '%s' not implemented." % module 80 | 81 | def handle_action(self, action): 82 | if action.startswith('module_'): 83 | self.switch_module(action[7:]) 84 | else: 85 | if hasattr(self, 'active'): 86 | self.active.handle_action(action) 87 | 88 | def handle_event(self, event): 89 | if event.type == pygame.KEYDOWN: 90 | if (event.key == pygame.K_ESCAPE): 91 | self.running = False 92 | else: 93 | if event.key in config.ACTIONS: 94 | self.handle_action(config.ACTIONS[event.key]) 95 | elif event.type == pygame.QUIT: 96 | self.running = False 97 | elif event.type == config.EVENTS['SONG_END']: 98 | if hasattr(config, 'radio'): 99 | config.radio.handle_event(event) 100 | else: 101 | if hasattr(self, 'active'): 102 | self.active.handle_event(event) 103 | 104 | def run(self): 105 | self.running = True 106 | while self.running: 107 | for event in pygame.event.get(): 108 | self.handle_event(event) 109 | self.update() 110 | self.render() 111 | self.check_gpio_input() 112 | pygame.time.wait(10) 113 | 114 | try: 115 | pygame.mixer.quit() 116 | except: 117 | pass 118 | -------------------------------------------------------------------------------- /pypboy/data.py: -------------------------------------------------------------------------------- 1 | import xmltodict 2 | import requests 3 | import numpy 4 | from numpy.fft import fft 5 | from math import log10 6 | import math 7 | import pygame 8 | 9 | 10 | class Maps(object): 11 | 12 | nodes = {} 13 | ways = [] 14 | tags = [] 15 | origin = None 16 | width = 0 17 | height = 0 18 | 19 | SIG_PLACES = 3 20 | GRID_SIZE = 0.001 21 | 22 | def __init__(self, *args, **kwargs): 23 | super(Maps, self).__init__(*args, **kwargs) 24 | 25 | def float_floor_to_precision(self, value, precision): 26 | for i in range(precision): 27 | value *= 10 28 | value = math.floor(value) 29 | for i in range(precision): 30 | value /= 10 31 | return value 32 | 33 | def fetch_grid(self, coords): 34 | # lat = self.float_floor_to_precision(coords[0], self.SIG_PLACES) 35 | # lng = self.float_floor_to_precision(coords[1], self.SIG_PLACES) 36 | # print lat, lng 37 | lat = coords[0] 38 | lng = coords[1] 39 | 40 | return self.fetch_area([ 41 | lat - self.GRID_SIZE, 42 | lng - self.GRID_SIZE, 43 | lat + self.GRID_SIZE, 44 | lng + self.GRID_SIZE 45 | ]) 46 | 47 | def fetch_area(self, bounds): 48 | self.width = (bounds[2] - bounds[0]) / 2 49 | self.height = (bounds[3] - bounds[1]) / 2 50 | self.origin = ( 51 | bounds[0] + self.width, 52 | bounds[1] + self.height 53 | ) 54 | url = "http://www.openstreetmap.org/api/0.6/map?bbox=%f,%f,%f,%f" % ( 55 | bounds[0], 56 | bounds[1], 57 | bounds[2], 58 | bounds[3] 59 | ) 60 | print "[Fetching maps... (%f, %f) to (%f, %f)]" % ( 61 | bounds[0], 62 | bounds[1], 63 | bounds[2], 64 | bounds[3] 65 | ) 66 | while True: 67 | try: 68 | response = requests.get(url) 69 | except: 70 | pass 71 | else: 72 | break 73 | osm_dict = xmltodict.parse(response.text.encode('UTF-8')) 74 | try: 75 | for node in osm_dict['osm']['node']: 76 | self.nodes[node['@id']] = node 77 | if 'tag' in node: 78 | for tag in node['tag']: 79 | try: 80 | #Named Amenities 81 | if tag["@k"] == "name": 82 | for tag2 in node['tag']: 83 | if tag2["@k"] == "amenity": 84 | amenity = tag2["@v"] 85 | self.tags.append((float(node['@lat']), float(node['@lon']), tag["@v"], amenity)) 86 | #Personal Addresses - Removed 87 | #if tag["@k"] == "addr:housenumber": 88 | # for t2 in node['tag']: 89 | # if t2["@k"] == "addr:street": 90 | # self.tags.append((float(node['@lat']), float(node['@lon']),tag["@v"]+" "+t2["@v"])) 91 | except Exception, e: 92 | pass 93 | 94 | for way in osm_dict['osm']['way']: 95 | waypoints = [] 96 | for node_id in way['nd']: 97 | node = self.nodes[node_id['@ref']] 98 | waypoints.append((float(node['@lat']), float(node['@lon']))) 99 | self.ways.append(waypoints) 100 | except Exception, e: 101 | print e 102 | #print response.text 103 | 104 | def fetch_by_coordinate(self, coords, range): 105 | return self.fetch_area(( 106 | coords[0] - range, 107 | coords[1] - range, 108 | coords[0] + range, 109 | coords[1] + range 110 | )) 111 | 112 | def transpose_ways(self, dimensions, offset, flip_y=True): 113 | width = dimensions[0] 114 | height = dimensions[1] 115 | w_coef = width / self.width / 2 116 | h_coef = height / self.height / 2 117 | transways = [] 118 | for way in self.ways: 119 | transway = [] 120 | for waypoint in way: 121 | lat = waypoint[1] - self.origin[0] 122 | lng = waypoint[0] - self.origin[1] 123 | wp = [ 124 | (lat * w_coef) + offset[0], 125 | (lng * h_coef) + offset[1] 126 | ] 127 | if flip_y: 128 | wp[1] *= -1 129 | wp[1] += offset[1] * 2 130 | transway.append(wp) 131 | transways.append(transway) 132 | return transways 133 | 134 | def transpose_tags(self, dimensions, offset, flip_y=True): 135 | width = dimensions[0] 136 | height = dimensions[1] 137 | w_coef = width / self.width / 2 138 | h_coef = height / self.height / 2 139 | transtags = [] 140 | for tag in self.tags: 141 | lat = tag[1] - self.origin[0] 142 | lng = tag[0] - self.origin[1] 143 | wp = [ 144 | tag[2], 145 | (lat * w_coef) + offset[0], 146 | (lng * h_coef) + offset[1], 147 | tag[3] 148 | ] 149 | if flip_y: 150 | wp[2] *= -1 151 | wp[2] += offset[1] * 2 152 | transtags.append(wp) 153 | return transtags 154 | 155 | 156 | 157 | class SoundSpectrum: 158 | """ 159 | Obtain the spectrum in a time interval from a sound file. 160 | """ 161 | 162 | left = None 163 | right = None 164 | 165 | def __init__(self, filename, force_mono=False): 166 | """ 167 | Create a new SoundSpectrum instance given the filename of 168 | a sound file pygame can read. If the sound is stereo, two 169 | spectra are available. Optionally mono can be forced. 170 | """ 171 | # Get playback frequency 172 | nu_play, format, stereo = pygame.mixer.get_init() 173 | self.nu_play = 1./nu_play 174 | self.format = format 175 | self.stereo = stereo 176 | 177 | # Load sound and convert to array(s) 178 | sound = pygame.mixer.Sound(filename) 179 | a = pygame.sndarray.array(sound) 180 | a = numpy.array(a) 181 | if stereo: 182 | if force_mono: 183 | self.stereo = 0 184 | self.left = (a[:,0] + a[:,1])*0.5 185 | else: 186 | self.left = a[:,0] 187 | self.right = a[:,1] 188 | else: 189 | self.left = a 190 | 191 | def get(self, data, start, stop): 192 | """ 193 | Return spectrum of given data, between start and stop 194 | time in seconds. 195 | """ 196 | duration = stop-start 197 | # Filter data 198 | start = int(start/self.nu_play) 199 | stop = int(stop/self.nu_play) 200 | N = stop - start 201 | data = data[start:stop] 202 | 203 | # Get frequencies 204 | frequency = numpy.arange(N/2)/duration 205 | 206 | # Calculate spectrum 207 | spectrum = fft(data)[1:1+N/2] 208 | power = (spectrum).real 209 | 210 | return frequency, power 211 | 212 | def get_left(self, start, stop): 213 | """ 214 | Return spectrum of the left stereo channel between 215 | start and stop times in seconds. 216 | """ 217 | return self.get(self.left, start, stop) 218 | 219 | def get_right(self, start, stop): 220 | """ 221 | Return spectrum of the left stereo channel between 222 | start and stop times in seconds. 223 | """ 224 | return self.get(self.right, start, stop) 225 | 226 | def get_mono(self, start, stop): 227 | """ 228 | Return mono spectrum between start and stop times in seconds. 229 | Note: this only works if sound was loaded as mono or mono 230 | was forced. 231 | """ 232 | return self.get(self.left, start, stop) 233 | 234 | class LogSpectrum(SoundSpectrum): 235 | """ 236 | A SoundSpectrum where the spectrum is divided into 237 | logarithmic bins and the logarithm of the power is 238 | returned. 239 | """ 240 | 241 | def __init__(self, filename, force_mono=False, bins=20, start=1e2, stop=1e4): 242 | """ 243 | Create a new LogSpectrum instance given the filename of 244 | a sound file pygame can read. If the sound is stereo, two 245 | spectra are available. Optionally mono can be forced. 246 | The number of spectral bins as well as the frequency range 247 | can be specified. 248 | """ 249 | SoundSpectrum.__init__(self, filename, force_mono=force_mono) 250 | start = log10(start) 251 | stop = log10(stop) 252 | step = (stop - start)/bins 253 | self.bins = 10**numpy.arange(start, stop+step, step) 254 | 255 | def get(self, data, start, stop): 256 | """ 257 | Return spectrum of given data, between start and stop 258 | time in seconds. Spectrum is given as the log of the 259 | power in logatithmically equally sized bins. 260 | """ 261 | f, p = SoundSpectrum.get(self, data, start, stop) 262 | bins = self.bins 263 | length = len(bins) 264 | result = numpy.zeros(length) 265 | ind = numpy.searchsorted(bins, f) 266 | for i,j in zip(ind, p): 267 | if i item and self.callbacks[item]: 92 | self.callbacks[item]() 93 | 94 | def handle_action(self, action): 95 | if action == "dial_up": 96 | if self.selected > 0: 97 | if config.SOUND_ENABLED: 98 | self.dial_move_sfx.play() 99 | self.select(self.selected - 1) 100 | if action == "dial_down": 101 | if self.selected < len(self.items) - 1: 102 | if config.SOUND_ENABLED: 103 | self.dial_move_sfx.play() 104 | self.select(self.selected + 1) 105 | 106 | def redraw(self): 107 | self.image.fill((0, 0, 0)) 108 | offset = 5 109 | for i in range(len(self.items)): 110 | text = config.FONTS[14].render(" %s " % self.items[i], True, (105, 255, 187), (0, 0, 0)) 111 | if i == self.selected: 112 | selected_rect = (5, offset - 2, text.get_size()[0] + 6, text.get_size()[1] + 3) 113 | pygame.draw.rect(self.image, (95, 255, 177), selected_rect, 2) 114 | self.image.blit(text, (10, offset)) 115 | offset += text.get_size()[1] + 6 116 | 117 | 118 | class Scanlines(game.Entity): 119 | 120 | def __init__(self, width, height, gap, speed, colours, full_push=False): 121 | super(Scanlines, self).__init__((width, height)) 122 | self.width = width 123 | self.height = height 124 | self.move = gap * len(colours) 125 | self.gap = gap 126 | self.colours = colours 127 | self.rect[1] = 0 128 | self.top = 0.0 129 | self.speed = speed 130 | self.full_push =full_push 131 | colour = 0 132 | area = pygame.Rect(0, self.rect[1] * self.speed, self.width, self.gap) 133 | while area.top <= self.height - self.gap: 134 | self.image.fill(self.colours[colour], area) 135 | area.move_ip(0, (self.gap)) 136 | colour += 1 137 | if colour >= len(self.colours): 138 | colour = 0 139 | 140 | def render(self, interval, *args, **kwargs): 141 | self.top += self.speed * interval 142 | self.rect[1] = self.top 143 | self.dirty = 1 144 | if self.full_push: 145 | if self.top >= self.height: 146 | self.top = 0 147 | else: 148 | if (self.top * self.speed) >= self.move: 149 | self.top = 0 150 | super(Scanlines, self).render(self, *args, **kwargs) 151 | 152 | 153 | class Overlay(game.Entity): 154 | def __init__(self): 155 | self.image = pygame.image.load('images/overlay.png') 156 | super(Overlay, self).__init__((config.WIDTH, config.HEIGHT)) 157 | self.blit_alpha(self, self.image, (0, 0), 128) 158 | 159 | def blit_alpha(self, target, source, location, opacity): 160 | x = location[0] 161 | y = location[1] 162 | temp = pygame.Surface((source.get_width(), source.get_height())).convert() 163 | temp.blit(target, (-x, -y)) 164 | temp.blit(source, (0, 0)) 165 | temp.set_alpha(opacity) 166 | target.blit(temp, location) 167 | 168 | 169 | class Border(game.Entity): 170 | def __init__(self): 171 | super(Border, self).__init__() 172 | self.image = pygame.image.load('images/border.png') 173 | self.rect = self.image.get_rect() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygame 2 | requests 3 | xmltodict 4 | numpy 5 | -------------------------------------------------------------------------------- /sounds/dial_move.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/sounds/dial_move.ogg -------------------------------------------------------------------------------- /sounds/module_change.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/sounds/module_change.ogg -------------------------------------------------------------------------------- /sounds/submodule_change.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grieve/pypboy/92970d3a53f86eba4dc99fc979c5d8682edd2b7e/sounds/submodule_change.ogg --------------------------------------------------------------------------------