Skip to content

Commit

Permalink
Refactor Storage API to load/save automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
lordmauve committed May 7, 2019
1 parent b9a54cc commit b617ee0
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 93 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ doc/_build
*.egg-info
.DS_Store
examples_dev
.tox
105 changes: 73 additions & 32 deletions doc/builtins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ The tween argument can be one of the following:
+--------------------+------------------------------------------------------+----------------------------------------+
| 'decelerate' | Start fast and decelerate to finish | .. image:: images/decelerate.png |
+--------------------+------------------------------------------------------+----------------------------------------+
| 'accel_decel' | Accelerate to mid point and decelerate to finish | .. image:: images/accel_decel.png |
| 'accel_decel' | Accelerate to mid point and decelerate to finish | .. image:: images/accel_decel.png |
+--------------------+------------------------------------------------------+----------------------------------------+
| 'in_elastic' | Give a little wobble at the end | .. image:: images/in_elastic.png |
+--------------------+------------------------------------------------------+----------------------------------------+
Expand Down Expand Up @@ -806,55 +806,96 @@ This could be used in a Pygame Zero program like this::
Data Storage
------------

The ``storage`` object behaves just like a dictionary but has two additional methods to
allow to save/load the data to/from the disk.
The ``storage`` object behaves just like a Python dictionary but its contents
are preserved across game sessions. The values you assign to storage will be
saved as JSON_, which means you can only store certain types of objects in it:
``list``/``tuple``, ``dict``, ``str``, ``float``/``int``, ``bool``, and
``None``.

Because it's a dictionary-like object, it supports all operations a dictionary does.
For example, you can update the storage with another dictionary, like so::
.. _JSON: https://en.wikipedia.org/wiki/JSON

my_data = {'player_turn': 1, 'level': 10}
storage.update(my_data)
The ``storage`` for a game is initially empty. Your code will need to handle
the case that values are loaded as well as the case that no values are found.

On windows the data is saved under ``%APPDATA%/pgzero/saves/`` and on Linux/MacOS under ``~/.config/pgzero/saves/``.
A tip is to use ``setdefault()``, which inserts a default if there is none::

The saved files will be named after their module name.
storage.setdefault('highscore', 0)

**NOTE:** Make sure your scripts have different names, otherwise they will be picking each other data.
Now, ``storage['highscore']`` will contain a value - ``0`` if there was no
value loaded, or the loaded value otherwise. You could add all of your
``setdefault`` lines towards the top of your game, before anything else looks
at ``storage``::

.. class:: Storage
storage.setdefault('level', 1)
storage.setdefault('player_name', 'Anonymous')
storage.setdefault('inventory', [])

Now, during gameplay we can update some values::

if player.colliderect(mushroom):
score += 5
if score > storage['highscore']:
storage['highscore'] = score

You can read them back at any time::

def draw():
...
screen.draw.text('Highscore: ' + storage['highscore'], ...)

...and of course, they'll be preserved when the game next launches.

These are some of the most useful methods of ``storage``:

.. class:: Storage(dict)

.. method:: storage[key] = value

Set a value in the storage.

.. method:: storage[key]

Get a value from the storage. Raise KeyError if there is no such key
in the storage.

.. method:: get(key, default=None)

Get a value from the storage. If there is no such key, return default,
or None if no default was given.

.. method:: clear()

Remove all stored values. Use this if you get into a bad state.

.. method:: setdefault(key, default)

Insert a default value into the storage, only if no value already
exists for this key.

.. method:: save()

Saves the data to disk.
Saves the data to disk now. You don't usually need to call this, unless
you're planning on using ``load()`` to reload a checkpoint, for
example.

.. method:: load()

Loads the data from disk.
Reload the contents of the storage with data from the save file. This
will replace any existing data in the storage.

Example of usage::
.. attribute:: path

# Setting some values
storage['my_score'] = 500
storage['level'] = 1
The actual path to which the save data will be written.

# You can have nested lists and dictionaries
storage['high_scores'] = []
storage['high_scores'].append(10)
storage['high_scores'].append(12)
storage['high_scores'].append(11)
storage['high_scores'].sort()

# Save storage to disk.
storage.save()
.. caution::

As you make changes to your game, ``storage`` could contain values that
don't work with your current code. You can either check for this, or call
``.clear()`` to remove all old values, or delete the save game file.

Following on the previous example, when starting your program, you can load that data back in::

storage.load()

my_score = storage['my_score']
.. tip::

level = storage['level']
Remember to check that your game still works if the storage is empty!

# Can use the get method from dicts to return a default value
storage.get('lifes', 3)
2 changes: 1 addition & 1 deletion pgzero/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

__version__ = '1.2'
__version__ = '1.3.dev0'
10 changes: 9 additions & 1 deletion pgzero/clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
'Clock', 'schedule', 'schedule_interval', 'unschedule'
]

# This type can't be weakreffed in Python 3.4
builtin_function_or_method = type(open)


def weak_method(method):
"""Quick weak method ref in case users aren't using Python 3.4"""
Expand All @@ -32,7 +35,12 @@ def mkref(o):
if isinstance(o, MethodType):
return weak_method(o)
else:
return ref(o)
try:
return ref(o)
except TypeError:
if isinstance(o, builtin_function_or_method):
return lambda: o
raise


@total_ordering
Expand Down
60 changes: 31 additions & 29 deletions pgzero/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from . import loaders
from . import clock
from . import builtins
from .storage import Storage
from . import storage


# The base URL for Pygame Zero documentation
Expand Down Expand Up @@ -152,8 +152,7 @@ def prepare_mod(mod):
Sprite surfaces for blitting to the screen).
"""
fn_hash = hash(mod.__file__) % ((sys.maxsize + 1) * 2)
Storage.set_app_hash(format(fn_hash, 'x'))
storage.storage._set_filename_from_path(mod.__file__)
loaders.set_root(mod.__file__)

# An icon needs to exist before the window is created.
Expand Down Expand Up @@ -197,29 +196,32 @@ def run_mod(mod, repl=False):
If `repl` is True, also run a REPL to interact with the module.
"""
game = PGZeroGame(mod)
if repl:
import asyncio
from ptpython.repl import embed
loop = asyncio.get_event_loop()

# Make sure the game runs
# NB. if the game exits, the REPL will keep running, which allows
# inspecting final state
game_task = loop.create_task(game.run_as_coroutine())

# Wait for the REPL to exit
loop.run_until_complete(embed(
globals=vars(mod),
return_asyncio_coroutine=True,
patch_stdout=True,
title="Pygame Zero REPL",
configure=configure_repl,
))

# Ask game loop to shut down (if it has not) and wait for it
if game.running:
pygame.event.post(pygame.event.Event(pygame.QUIT))
loop.run_until_complete(game_task)
else:
game.run()
try:
game = PGZeroGame(mod)
if repl:
import asyncio
from ptpython.repl import embed
loop = asyncio.get_event_loop()

# Make sure the game runs
# NB. if the game exits, the REPL will keep running, which allows
# inspecting final state
game_task = loop.create_task(game.run_as_coroutine())

# Wait for the REPL to exit
loop.run_until_complete(embed(
globals=vars(mod),
return_asyncio_coroutine=True,
patch_stdout=True,
title="Pygame Zero REPL",
configure=configure_repl,
))

# Ask game loop to shut down (if it has not) and wait for it
if game.running:
pygame.event.post(pygame.event.Event(pygame.QUIT))
loop.run_until_complete(game_task)
else:
game.run()
finally:
storage.Storage.save_all()

0 comments on commit b617ee0

Please sign in to comment.