--- a/duckmaze.py +++ b/duckmaze.py @@ -19,10 +19,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -import pygame, os, sys, random, gc, md5 +import pygame, os, sys, random, gc +import asyncio +import webloop from pygame.locals import * from mopelib import mopelib -import level_editor import level #import time @@ -188,31 +189,15 @@ menu.add_item( "Start game", MENU_START ) menu.add_item( "Start at level: %d" % (config.start_level+1), MENU_START_LEVEL ) - menu.add_item( "Level editor", MENU_LEVEL_EDITOR ) elif gamestate.alive == level.INGAME_ALIVE: menu.add_item( "Continue", MENU_START ) menu.add_item( "Restart level", MENU_RESTART ) menu.add_item( "End game", MENU_END ) - tmp_str = "Music: " - if config.music_on: - tmp_str += "on" - else: - tmp_str += "off" - menu.add_item( tmp_str, MENU_MUSIC ) - - tmp_str = "Effects: " - if config.sound_effects_on: - tmp_str += "on" - else: - tmp_str += "off" - menu.add_item( tmp_str, MENU_SOUND_EFFECTS ) - - menu.add_item( "Quit duckmaze", MENU_QUIT ) return menu -def general_menu_screen( config, gamestate ): +async def general_menu_screen( config, gamestate ): if gamestate == None: menu_title = "duckmaze" @@ -228,7 +213,7 @@ waiting = True while waiting: - event = pygame.event.wait() + event = await webloop.wait() if event.type == QUIT: sys.exit(0) elif config.keys_menu.matches( event ): @@ -274,14 +259,6 @@ general_menu_create_menu( menu, config, gamestate ) menurender.repaint_full() config.save() - elif code == MENU_LEVEL_EDITOR: - lve = level_editor.LevelEditor( config, screen, sound_mgr ) - lve.mainloop() - if config.edit_real_levels: - levels = load_levels( config ) - menurender.repaint_full() - elif code == MENU_QUIT: - sys.exit(0) return game_start @@ -305,7 +282,7 @@ intro_draw_instructions() pygame.display.update() -def intro_input( event, config, intro_mode ): +async def intro_input( event, config, intro_mode ): if event.type == QUIT: sys.exit(0) elif config.keys_volup.matches( event ): @@ -317,32 +294,34 @@ else: if event.type == EVENTTYPE_TITLE: intro_mode += 1 + if intro_mode == INTRO_MODE_MUSIC: + intro_mode += 1 if intro_mode == INTRO_MODE_ENDED: intro_mode = INTRO_MODE_TITLE intro_draw_something( intro_mode ) elif config.keys_menu.matches( event ): mopelib.clear_events( EVENTTYPE_TITLE ) - start_game = general_menu_screen( config, None ) + start_game = await general_menu_screen( config, None ) if start_game: intro_mode = INTRO_MODE_ENDED else: intro_draw_something( intro_mode ) - pygame.time.set_timer( EVENTTYPE_TITLE, TITLE_TICK_TIME ) + webloop.set_timer( EVENTTYPE_TITLE, TITLE_TICK_TIME ) elif config.keys_startgame.matches( event ): intro_mode = INTRO_MODE_ENDED return intro_mode # ---------------------- -def intro_mainloop( config ): +async def intro_mainloop( config ): intro_draw_title() pygame.display.update() - pygame.time.set_timer( EVENTTYPE_TITLE, TITLE_TICK_TIME ) + webloop.set_timer( EVENTTYPE_TITLE, TITLE_TICK_TIME ) intro_mode = INTRO_MODE_TITLE while intro_mode < INTRO_MODE_ENDED: - intro_mode = intro_input( pygame.event.wait(), config, intro_mode ) + intro_mode = await intro_input( await webloop.wait(), config, intro_mode ) mopelib.clear_events( EVENTTYPE_TITLE ) @@ -406,12 +385,12 @@ # ---------------------- -def inlevel_input( event, gamestate ): +async def inlevel_input( event, gamestate ): if event.type == QUIT: sys.exit(0) elif( ( event.type == pygame.ACTIVEEVENT and event.state == 2 ) or config.keys_menu.matches( event ) ): - general_menu_screen( config, gamestate ) + await general_menu_screen( config, gamestate ) gamestate.old_dim = -2 inlevel_redraw_screen( gamestate ) elif config.keys_volup.matches( event ): @@ -426,7 +405,7 @@ elif event.type == EVENTTYPE_TIMER: gamestate.timer_tick() if gamestate.alive == level.INGAME_DEAD: - finishedlevel_mainloop( gamestate ) + await finishedlevel_mainloop( gamestate ) gamestate.same_level() else: inlevel_redraw_screen( gamestate ) @@ -439,22 +418,22 @@ #sound_mgr.play_sample( "waddles" ) gamestate.cur_lev.move_left( gamestate ) inlevel_redraw_screen( gamestate ) - pygame.time.set_timer( EVENTTYPE_MOVE, MOVE_TICK_TIME ) + webloop.set_timer( EVENTTYPE_MOVE, MOVE_TICK_TIME ) elif config.keys_right.matches( event ): #sound_mgr.play_sample( "waddles" ) gamestate.cur_lev.move_right( gamestate ) inlevel_redraw_screen( gamestate ) - pygame.time.set_timer( EVENTTYPE_MOVE, MOVE_TICK_TIME ) + webloop.set_timer( EVENTTYPE_MOVE, MOVE_TICK_TIME ) elif config.keys_up.matches( event ): #sound_mgr.play_sample( "waddles" ) gamestate.cur_lev.move_up( gamestate ) inlevel_redraw_screen( gamestate ) - pygame.time.set_timer( EVENTTYPE_MOVE, MOVE_TICK_TIME ) + webloop.set_timer( EVENTTYPE_MOVE, MOVE_TICK_TIME ) elif config.keys_down.matches( event ): #sound_mgr.play_sample( "waddles" ) gamestate.cur_lev.move_down( gamestate ) inlevel_redraw_screen( gamestate ) - pygame.time.set_timer( EVENTTYPE_MOVE, MOVE_TICK_TIME ) + webloop.set_timer( EVENTTYPE_MOVE, MOVE_TICK_TIME ) elif( ( gamestate.cur_lev.direction == level.DIR_LEFT and config.keys_left_release.matches( event ) ) or @@ -503,7 +482,7 @@ def start_fade( self ): self.dim = 2 - pygame.time.set_timer( EVENTTYPE_FADE, FADE_TICK_TIME ) + webloop.set_timer( EVENTTYPE_FADE, FADE_TICK_TIME ) def next_level( self ): self.level_num += 1 @@ -532,25 +511,25 @@ # ---------------------- -def ingame_mainloop( config, levels ): +async def ingame_mainloop( config, levels ): gamestate = GameState( config, levels ) while gamestate.alive == level.INGAME_ALIVE: - inlevel_mainloop( config, gamestate ) + await inlevel_mainloop( config, gamestate ) if gamestate.alive == level.INGAME_FINISHED_LEVEL: gamestate.next_level() return gamestate # ---------------------- -def inlevel_mainloop( config, gamestate ): +async def inlevel_mainloop( config, gamestate ): gamestate.start_fade() inlevel_redraw_screen( gamestate ) gc.disable() - pygame.time.set_timer( EVENTTYPE_TIMER, TIMER_TICK_TIME ) + webloop.set_timer( EVENTTYPE_TIMER, TIMER_TICK_TIME ) while gamestate.alive == level.INGAME_ALIVE: - inlevel_input( pygame.event.wait(), gamestate ) + await inlevel_input( await webloop.wait(), gamestate ) gc.enable() mopelib.clear_events( EVENTTYPE_TIMER ) @@ -598,7 +577,7 @@ # ---------------------- -def finishedlevel_mainloop( gamestate ): +async def finishedlevel_mainloop( gamestate ): mopelib.clear_events( EVENTTYPE_MOVE ) mopelib.clear_events( EVENTTYPE_FADE ) @@ -610,12 +589,12 @@ 0.05, 0.8 ) pygame.display.update() while waiting: - waiting = finishedgame_input( pygame.event.wait(), waiting ) + waiting = finishedgame_input( await webloop.wait(), waiting ) # ---------------------- -def finishedgame_mainloop( config, gamestate ): +async def finishedgame_mainloop( config, gamestate ): mopelib.clear_events( EVENTTYPE_MOVE ) mopelib.clear_events( EVENTTYPE_FADE ) @@ -631,7 +610,7 @@ 0.05, 0.8 ) pygame.display.update() while waiting: - waiting = finishedgame_input( pygame.event.wait(), waiting ) + waiting = finishedgame_input( await webloop.wait(), waiting ) ingame_surface_background.fill( config.colour_background ) @@ -727,11 +706,11 @@ num_args = len( sys.argv ) if num_args > 1: if sys.argv[1] == "--help": - print "Usage:" - print "duckmaze [install_dir] [config_file] [resolution]" - print - print "e.g. ./duckmaze.py . duckmazerc.txt '(640,480)'" - print + print("Usage:") + print("duckmaze [install_dir] [config_file] [resolution]") + print() + print("e.g. ./duckmaze.py . duckmazerc.txt '(640,480)'") + print() sys.exit( 0 ) install_dir = sys.argv[1] @@ -741,7 +720,11 @@ if num_args > 2: config_filename = sys.argv[2] else: - config_filename = os.path.expanduser( "~/.duckmaze/config" ) + if os.path.isdir( "/saves" ): + # on the web the page keeps the /saves folder in the browser + config_filename = "/saves/duckmaze-config" + else: + config_filename = os.path.expanduser( "~/.duckmaze/config" ) config = DuckMazeConfig( config_filename ) @@ -770,8 +753,8 @@ config.unsaved.append( "arena_size" ) screen_border = 5 -scale = ( ( config.screen_size[0] - screen_border*2 ) / config.arena_size[0], - ( config.screen_size[1] - screen_border*2 ) / (config.arena_size[1]+1) ) +scale = ( ( config.screen_size[0] - screen_border*2 ) // config.arena_size[0], + ( config.screen_size[1] - screen_border*2 ) // (config.arena_size[1]+1) ) # General initialisation @@ -799,12 +782,15 @@ menurender = mopelib.MenuRenderer( screen, config, ingame_surface_background, (32, 32, 32), (255, 255, 255), (75, 75, 75) ) -while True: - sound_mgr.music_loud() - intro_mainloop( config ) - sound_mgr.music_quiet() - cur_level = ingame_mainloop( config, levels ) - intro_mode = finishedgame_mainloop( config, cur_level ) +async def main(): + while True: + sound_mgr.music_loud() + await intro_mainloop( config ) + sound_mgr.music_quiet() + cur_level = await ingame_mainloop( config, levels ) + await finishedgame_mainloop( config, cur_level ) + +asyncio.run( main() ) --- a/level.py +++ b/level.py @@ -69,7 +69,7 @@ ln.append( 1 ) else: if isinstance( filename_or_level, str ): - self.construct_from_file( filename_or_level ) + self.construct_from_open( filename_or_level ) else: self.construct_from_level( filename_or_level ) @@ -97,8 +97,8 @@ for other_i in other_ln: self_ln.append( other_i ) - def save_to_file( self, filename ): - fl = file( filename, 'w' ) + def save_to_open( self, filename ): + fl = open( filename, 'w' ) fl.write( "time = %d\n" % self.total_time ) fl.write( "background_colour = (%d, %d, %d)\n" @@ -142,13 +142,13 @@ os.fsync( fl.fileno() ) fl.close() - def construct_from_file( self, filename ): + def construct_from_open( self, filename ): MODE_CFG = 0 MODE_HOR = 1 MODE_VER = 2 - lev_file = file( filename, "r" ) + lev_file = open( filename, "r" ) self.direction = DIR_RIGHT @@ -178,7 +178,7 @@ elif k == "text_colour": self.text_colour = v else: - print "Unknown level config key: '%s'" % k + print("Unknown level config key: '%s'" % k) if mode == MODE_HOR: hor_line = [] --- a/mopelib/mopelib.py +++ b/mopelib/mopelib.py @@ -19,7 +19,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -import pygame, os, sys, random, gc, md5 +import pygame, os, sys, random, gc, hashlib +import webloop from pygame.locals import * # ---------------------- @@ -81,7 +82,7 @@ # ---------------------- def read_version( config ): - f = file( os.path.join( config.install_dir, "version" ), 'r' ) + f = open( os.path.join( config.install_dir, "version" ), 'r' ) ln = f.readline() f.close() @@ -121,8 +122,7 @@ # ---------------------- def clear_events( event_type ): - pygame.time.set_timer( event_type, 0 ) - pygame.event.clear( event_type ) + webloop.clear_events( event_type ) class Hiscores: def __init__( self, hiscores_filename, default_score_table ): @@ -132,11 +132,11 @@ try: self.read_scores() except HashException: - print "Fiddling detected: high scores reset to defaults!" + print("Fiddling detected: high scores reset to defaults!") self.scores = default_score_table if len( self.scores[0] ) != self.num_tables: - print "Creating high score table." + print("Creating high score table.") self.scores = default_score_table def read_scores( self ): @@ -144,7 +144,7 @@ next_hash = None try: - f = file( self.filename, 'r' ) + f = open( self.filename, 'r' ) ln = f.readline() lines_since_hash = 5 while( ln ): @@ -184,8 +184,8 @@ ans += str( pair[1] ) ans += "|" - m = md5.new() - m.update( ans ) + m = hashlib.md5() + m.update( ans.encode( "utf-8" ) ) return m.hexdigest() def save_array( self, f, array ): @@ -195,7 +195,7 @@ def save( self ): mkdir_if_needed( self.filename ) - f = file( self.filename, 'w' ) + f = open( self.filename, 'w' ) for arr in self.scores: self.save_array( f, arr ) f.flush() @@ -213,7 +213,7 @@ self.default_config() try: - f = file( config_filename, 'r' ) + f = open( config_filename, 'r' ) ln = f.readline() while( ln ): self.process_line( ln ) @@ -254,8 +254,8 @@ value[1:-1].split( ',' ) ) ) elif value[0] == "[" and value[-1] == "]": - return map( self.parse_value, - value[1:-1].split( ',' ) ) + return list( map( self.parse_value, + value[1:-1].split( ',' ) ) ) elif ( value[0] == '"' and value[-1] == '"' ) \ or ( value[0] == "'" and value[-1] == "'" ): @@ -272,9 +272,8 @@ def save( self ): mkdir_if_needed( self.filename ) - f = file( self.filename, 'w' ) - keys = self.__dict__.keys() - keys.sort() + f = open( self.filename, 'w' ) + keys = sorted( self.__dict__.keys() ) for k in keys: if k not in self.unsaved: v = self.__dict__[k] @@ -472,10 +471,10 @@ self.sample_groups = {} self.config = config - if "music_on" not in config.__dict__: - config.music_on = 1 - if "sound_effects_on" not in config.__dict__: - config.sound_effects_on = 1 + if "music_on" not in config.__dict__: + config.music_on = 1 + if "sound_effects_on" not in config.__dict__: + config.sound_effects_on = 1 if music_filename != None: self.music_filename = os.path.join( config.music_dir, @@ -585,7 +584,7 @@ #print "load sample '%s'" % fn self.sample_groups[groupname].append( snd ) else: - print "Could not find sound file '%s'" % fn + print("Could not find sound file '%s'" % fn) def music_load( self ): if self.sound_inited: @@ -595,7 +594,7 @@ self.music_sample = pygame.mixer.Sound( self.music_filename ) #print "load music '%s'" % self.music_filename else: - print "Could not find music file '%s'." % self.music_filename + print("Could not find music file '%s'." % self.music_filename) def setup( self, gamestate ): if "volume" in self.config.__dict__: @@ -610,7 +609,7 @@ else: self.sound_inited = False if "init_error" not in self.__dict__: - print "Unable to initialise the sound mixer." + print("Unable to initialise the sound mixer.") self.init_error = True if self.sound_inited: --- a/webloop.py +++ b/webloop.py @@ -0,0 +1,36 @@ +# webloop.py - replaces pygame.event.wait() and pygame.time.set_timer() for the browser +# (neither can block or use threads there): the timers are kept here and polled while we wait. +import asyncio +import collections +import pygame + +_timers = {} +_queue = collections.deque() + + +def set_timer(event_type, milliseconds): + if milliseconds <= 0: + _timers.pop(event_type, None) + else: + _timers[event_type] = [milliseconds, pygame.time.get_ticks() + milliseconds] + + +def clear_events(event_type): + _timers.pop(event_type, None) + pygame.event.clear(event_type) + for ev in [e for e in _queue if e.type == event_type]: + _queue.remove(ev) + + +async def wait(): + """ the next event, giving the browser time to run while there is none """ + while True: + now = pygame.time.get_ticks() + for event_type, timer in list(_timers.items()): + if timer[1] <= now: + _queue.append(pygame.event.Event(event_type)) + timer[1] = max(timer[1] + timer[0], now) + _queue.extend(pygame.event.get()) + if _queue: + return _queue.popleft() + await asyncio.sleep(0)