--- a/lib20k/__init__.py +++ b/lib20k/__init__.py @@ -3,5 +3,5 @@ # This game is licensed under GPL v2, and copyright (C) Jack Whitham 2006-21. # -from .main import Main, PyInstaller_Main +from .main import Main from .events import Events --- a/lib20k/config.py +++ b/lib20k/config.py @@ -69,6 +69,9 @@ def Get_Home() -> Optional[str]: + if os.path.isdir("/saves"): + # on the web the page keeps the /saves folder in the browser + return "/saves" for i in [ "HOME", "APPDATA" ]: # NO-COV home = os.getenv(i) # NO-COV if ( home is not None ): # NO-COV --- a/lib20k/events.py +++ b/lib20k/events.py @@ -3,7 +3,8 @@ # This game is licensed under GPL v2, and copyright (C) Jack Whitham 2006-21. # -import pygame, webbrowser, urllib.request +import asyncio +import pygame from .game_types import * from .primitives import * from . import mail, config, compatibility @@ -55,20 +56,13 @@ def poll(self) -> Event: # NO-COV return Event(pygame.event.poll(), self) - def wait(self) -> Event: # NO-COV - return Event(pygame.event.wait(), self) - - def webbrowser_open(self, url: str) -> None: # NO-COV - pygame.display.iconify() - try: - webbrowser.open(url, True, True) - except Exception: - pass # NO-COV - - def check_update(self, url: str) -> str: # NO-COV - with urllib.request.urlopen(url) as f: - new_version_bytes = f.readline() - return new_version_bytes.decode("ascii") + async def wait(self) -> Event: # NO-COV + # (pygame.event.wait would block the browser: poll and yield instead) + while True: + e = self.poll() + if e.type != pygame.NOEVENT: + return e + await asyncio.sleep(0.01) def resurface(self) -> SurfaceType: # NO-COV --- a/lib20k/game.py +++ b/lib20k/game.py @@ -6,6 +6,7 @@ # The main loop of the game. This procedure is running # whenever the game is on the screen. +import asyncio import pygame, sys, time, pickle, random, os, math from . import draw_effects, stats, mail, gametime, events @@ -249,14 +250,28 @@ self.g.game_time.Get_Day(), self.screen.get_rect().height))) New_Mail("SCREENSHOT CHEAT") - def Main_Loop(self) -> bool: + next_frame_time = 0.0 + + async def Frame_Delay(self, fps: int) -> None: + # Replaces Clock.tick, which would block the browser: sleep until + # the next frame is due (and do not try to catch up after a long stall) + now = time.monotonic() + if fps <= 0: + self.next_frame_time = now + await asyncio.sleep(0) + return + self.next_frame_time += 1.0 / fps + if self.next_frame_time < now - 0.25: + self.next_frame_time = now + await asyncio.sleep(max(0.0, self.next_frame_time - now)) + + async def Main_Loop(self) -> bool: alarm_sound = sound.Persisting_Sound(Sounds.emergency) g = self.g # menu setup exit_options: List[MenuItem] = [ - (MenuCommand.MENU, "Exit to Main Menu", [ pygame.K_q ]), - (MenuCommand.QUIT, "Exit Game", [ pygame.K_F10 ])] + (MenuCommand.MENU, "Exit to Main Menu", [ pygame.K_q ])] save_available: List[MenuItem] = [ (MenuCommand.SAVE, "Save Game", [pygame.K_s]), @@ -311,11 +326,11 @@ if self.ui.Is_Fast_Forward(): - self.clock.tick(FRAME_RATE * 10) + await self.Frame_Delay(FRAME_RATE * 10) elif (self.playback_mode in (PlayMode.PLAYBACK, PlayMode.PLAYTHRU)): - self.clock.tick(0) + await self.Frame_Delay(0) else: - self.clock.tick(FRAME_RATE) + await self.Frame_Delay(FRAME_RATE) if not paused: flash = not flash @@ -513,7 +528,7 @@ # Events if paused: - e = self.event.wait() + e = await self.event.wait() else: e = self.event.poll() @@ -665,7 +680,7 @@ pygame.display.flip() if ( stats_review ): - review.Review(g, g.historian, self.event) + await review.Review(g, g.historian, self.event) return quit --- a/lib20k/main.py +++ b/lib20k/main.py @@ -4,8 +4,9 @@ # +import asyncio import pygame, sys, math, time, os -import getopt, traceback, tempfile +import getopt from . import game, font, save_menu, resource, menu, events @@ -15,7 +16,7 @@ from .game_random import PlaybackEOF -def Main(data_dir: str, args: List[str], event: events.Events) -> int: +async def Main(data_dir: str, args: List[str], event: events.Events) -> int: print("") print(TITLE) @@ -83,7 +84,7 @@ else: pygame.mixer.init(44100,-16,2,bufsize) - flags = pygame.RESIZABLE + flags = 0 # Force a specific window height for testing purposes if "--test-height" in opts: @@ -119,7 +120,7 @@ # record/playback try: return_code = 1 # Assume playback did not complete - game.Game(clock=clock, + await game.Game(clock=clock, restore_pos=None, challenge=record_challenge, event=event, playback_mode=playback_mode, @@ -131,10 +132,10 @@ quit = True - while ( not quit ): - quit = Main_Menu_Loop(TITLE, clock, event) - - config.Save() + # There is nowhere to exit to in the browser: the menu simply starts again + while True: + await Main_Menu_Loop(TITLE, clock, event) + config.Save() # Bye bye Pygame. if event.is_testing: @@ -147,7 +148,7 @@ return return_code -def Main_Menu_Loop(name: str, clock: ClockType, +async def Main_Menu_Loop(name: str, clock: ClockType, event: events.Events) -> bool: current_menu: menu.Menu @@ -158,11 +159,7 @@ (MenuCommand.LOAD, "Restore Game", [pygame.K_r]), (None, None, []), menu.TOGGLE_SOUND, - (MenuCommand.MANUAL, "View Manual", [pygame.K_v]), - (MenuCommand.UPDATES, "Check for Updates", [pygame.K_u]), - (None, None, []), - (MenuCommand.QUIT, "Exit", - [ pygame.K_ESCAPE , pygame.K_F10 ])]) + (None, None, [])]) difficulty_menu = menu.Menu( [(None, None, []), (MenuCommand.TUTORIAL, "Tutorial", [pygame.K_t]), @@ -211,7 +208,7 @@ screen.blit(img, img_r.topleft) y += img_r.height - (quit, cmd) = menu.Simple_Menu_Loop(screen, current_menu, + (quit, cmd) = await menu.Simple_Menu_Loop(screen, current_menu, (( width * 3 ) // 4, 10 + ( height // 2 )), event) if ( current_menu == main_menu ): @@ -219,7 +216,7 @@ current_menu = difficulty_menu elif ( cmd == MenuCommand.TUTORIAL ): - quit = game.Game(clock=clock, + quit = await game.Game(clock=clock, restore_pos=None, challenge=MenuCommand.TUTORIAL, event=event, playback_mode=PlayMode.OFF, @@ -229,29 +226,6 @@ elif ( cmd == MenuCommand.LOAD ): current_menu = save_menu.Save_Menu(False) - elif ( cmd == MenuCommand.QUIT ): - quit = True - - elif ( cmd == MenuCommand.UPDATES ): - if Update_Feature(menu_image, event): - url = ( CGISCRIPT + "v=" + version.Encode(VERSION) ) - - event.webbrowser_open(url) - - elif ( cmd == MenuCommand.MANUAL ): - if os.path.isfile(DEB_MANUAL): - # Debian manual present - url = 'file://' + DEB_MANUAL # NO-COV - else: - base = os.path.abspath("manual.pdf") - if os.path.isfile(base): - # Upstream package manual present - url = 'file://' + base - else: - # No manual on the disk? Redirect to website. - url = 'https://www.jwhitham.org/20kly/manual.pdf' # NO-COV - - event.webbrowser_open(url) elif ( current_menu == difficulty_menu ): if ( cmd != None ): @@ -259,7 +233,7 @@ if (( cmd != None ) and ( cmd != MenuCommand.CANCEL )): # start new game with specified difficulty - quit = game.Game(clock=clock, + quit = await game.Game(clock=clock, restore_pos=None, challenge=cmd, event=event, playback_mode=PlayMode.OFF, @@ -272,123 +246,10 @@ if (( cmd != None ) and ( cmd != MenuCommand.CANCEL )): # Start game from saved position - quit = game.Game(clock=clock, + quit = await game.Game(clock=clock, restore_pos=cmd, challenge=None, event=event, playback_mode=PlayMode.OFF, playback_file=None, record_file=None).Main_Loop() return True - -def Update_Feature(menu_image: SurfaceType, event: events.Events) -> bool: - screen = event.resurface() - - def Message(msg_list: List[str]) -> None: - screen.blit(menu_image, (0,0)) - - y = screen.get_rect().centery - for msg in msg_list: - img_1 = font.Get_Font(24).render(msg, True, (255, 255, 255)) - img_2 = font.Get_Font(24).render(msg, True, (0, 0, 0)) - img_r = img_1.get_rect() - img_r.centerx = screen.get_rect().centerx - img_r.centery = y - screen.blit(img_2, img_r.topleft) - screen.blit(img_1, img_r.move(2,-2).topleft) - y += img_r.height - pygame.display.flip() - - def Finish(cerror: Optional[str] = None) -> None: - if ( cerror is not None ): - Message(["Connection error:", cerror]) - - ok = True - timer = 4000 - while (( ok ) and ( timer > 0 )): - e = event.poll() - while ( e.type != pygame.NOEVENT ): - if (( e.type == pygame.MOUSEBUTTONDOWN ) - or ( e.type == pygame.KEYDOWN ) - or ( e.type == pygame.QUIT )): - ok = False - e = event.poll() - - if ok: # NO-COV - pygame.time.wait( 40 ) - timer -= 40 - - Message(["Connecting to Website..."]) - url = ( CGISCRIPT + "a=2" ) - new_version = None - try: - new_version = event.check_update(url) - except Exception as x: - Finish(str(x)) - return False - - if (( new_version is None ) - or ( type(new_version) != str ) - or ( len(new_version) < 2 ) - or ( len(new_version) > 10 ) - or ( not new_version[ 0 ].isdigit() ) - or ( new_version.find('.') <= 0 )): - Finish("Version data not found.") - return False - - - new_version_int = version.Decode(new_version) - new_version = version.Encode(new_version_int) - old_version_int = VERSION - old_version = version.Encode(old_version_int) - - if new_version_int == old_version_int: - Message(["Your software is up to date!", - "Thankyou for using the update feature."]) - Finish(None) - return False - elif new_version_int > old_version_int: - Message(["New version " + new_version + " is available!", - "Opening website..."]) - Finish(None) - return True - else: - Message(["Your software is very up to date (beta?)", - "'New' version is " + new_version + ": your version is " + old_version]) - Finish(None) - return False - -def PyInstaller_Main() -> None: # NO-COV - data_dir = os.path.join(sys.prefix, "data") - args = sys.argv[1:] - with tempfile.TemporaryFile("wt+", encoding="utf-8") as fd: - sys.stdout = typing.cast(typing.TextIO, fd) - sys.stderr = typing.cast(typing.TextIO, fd) - print("") - print("startup:", time.asctime(), flush=True) - try: - return_code = Main(data_dir=data_dir, args=args, - event=events.Events()) - except Exception: - print("") - print("exception:", flush=True) - traceback.print_exc(file=fd) - return_code = 1 - - if return_code == 0: - return - - print("return code:", return_code, flush=True) - fd.seek(0, 0) - all_log = fd.read() - - crash_dir = config.Get_Home() - if crash_dir is None: - crash_dir = tempfile.gettempdir() - - crash_file = os.path.join(crash_dir, "20kly-crash-info.txt") - open(crash_file, "at").write(all_log) - sys.exit(return_code) - - - - --- a/lib20k/menu.py +++ b/lib20k/menu.py @@ -258,7 +258,7 @@ TOGGLE_SOUND[0], text, TOGGLE_SOUND[2]) -def Simple_Menu_Loop(screen: SurfaceType, current_menu: Menu, +async def Simple_Menu_Loop(screen: SurfaceType, current_menu: Menu, xy: SurfacePosition, event: events.Events) -> Tuple[bool, Optional[MenuCommand]]: (x,y) = xy cmd = None @@ -268,7 +268,7 @@ current_menu.Draw(screen, (x,y)) pygame.display.flip() - e = event.wait() + e = await event.wait() while ( e.type != pygame.NOEVENT ): if e.type == pygame.QUIT: quit = True --- a/lib20k/review.py +++ b/lib20k/review.py @@ -31,7 +31,7 @@ return Historical_Record(game_object) # Called at the end of the game, to display statistics: -def Review(game_object: "game.Game_Data", +async def Review(game_object: "game.Game_Data", historian: List[Historical_Record], event: events.Events) -> None: @@ -213,7 +213,7 @@ screen = Regraph(available_graphs[ graph_num ]) (width, height) = screen.get_rect().size - (quit, cmd) = menu.Simple_Menu_Loop(screen, + (quit, cmd) = await menu.Simple_Menu_Loop(screen, proceed, (( width * 3 ) // 4, height // 2 ), event) if ( cmd == MenuCommand.MENU ): --- a/main.py +++ b/main.py @@ -0,0 +1,11 @@ +import pygame +import asyncio +import os +import sys + +# Browser launcher (replaces the "lightyears" script of the original) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from lib20k import Main, Events + +data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") +asyncio.run(Main(data_dir=data_dir, args=[], event=Events()))