Skip to content

Commit

Permalink
Merge pull request #180 from bitcraft/plugins
Browse files Browse the repository at this point in the history
move all states into plugin format, fix -S, stats
  • Loading branch information
Mekire committed Jul 10, 2015
2 parents 12ed03a + 739acd9 commit c562a67
Show file tree
Hide file tree
Showing 128 changed files with 923 additions and 6,208 deletions.
64 changes: 22 additions & 42 deletions data/components/casino_player.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import os
import inspect
import datetime
from importlib import import_module
from collections import OrderedDict
from data import prepare
from .. import prepare
from . import loggable
##from ..states.baccarat import Baccarat


class BankAccount(object):
Expand Down Expand Up @@ -81,37 +79,22 @@ class CasinoPlayer(loggable.Loggable):
allows persistence of player statistics between
sessions."""

# Game states for which no statistics are collected
no_stats_stats = [
'LOBBYSCREEN', 'STATSMENU', 'STATSSCREEN',
]

def __init__(self, stats=None):
def __init__(self, stats_init=None):
self.addLogger()
self._current_game = None

self._stats = OrderedDict([("cash", prepare.MONEY),
("account balance", 0)])
package = "data.states.games."
game_folder = os.path.join(".", "data", "states", "games")
exclude_endings = (".py", ".pyc", "__pycache__")
for folder in os.listdir(game_folder):
if any(folder.endswith(end) for end in exclude_endings):
continue
try:
game_module = import_module(package+folder)
self._stats[folder] = game_module.Game.initialize_stats()
except Exception as e:
template = "{} failed to load stats."
print(e)
print(template.format(folder))

if stats is not None:
self.cash = stats["cash"]
self.account_balance = stats["account balance"]
for game in self.game_names():
self.current_game = game
for stat_name in self.get_stat_names():
self.set(stat_name, stats[game].get(stat_name, self.get(stat_name)))

if stats_init is not None:
self.cash = stats_init.pop("cash")
self.account_balance = stats_init.pop("account balance")
for game_name, game_stats in stats_init.items():
self._stats[game_name] = OrderedDict()
self.current_game = game_name
for stat_name, value in game_stats.items():
self.set(stat_name, value)

self.account = BankAccount(self, self.account_balance)

@property
Expand Down Expand Up @@ -157,19 +140,16 @@ def current_game(self):
@current_game.setter
def current_game(self, value):
"""Set the current game"""
if value in self.no_stats_stats:
self._current_game = None
#
# The case of the the current game is not handled very consistently so
# the following code makes sure this works whatever the case is
# TODO: remove case inconsistencies and then remove this code
for name in self._stats:
if name.lower() == value.lower():
self._current_game = name
break
else:
#
# The case of the the current game is not handled very consistently so
# the following code makes sure this works whatever the case is
# TODO: remove case inconsistencies and then remove this code
for name in self._stats:
if name.lower() == value.lower():
self._current_game = name
break
else:
raise GameNotFound('There was no game called "{0}" in the stats collection'.format(value))
raise GameNotFound('There was no game called "{0}" in the stats collection'.format(value))

def increase(self, name, amount=1):
"""Increase the value of a stat"""
Expand Down
306 changes: 306 additions & 0 deletions data/control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
from collections import OrderedDict
from importlib import import_module
import os
import json
import pygame as pg
from data.components.casino_player import CasinoPlayer
from . import prepare



class Control(object):
"""
Control class for entire project. Contains the game loop, and contains
the event_loop which passes events to States as needed. Logic for flipping
states is also found here.
"""
def __init__(self, caption, render_size, resolutions):
self.screen = pg.display.get_surface()
self.screen_rect = self.screen.get_rect()
self.render_size = render_size
self.render_surf = pg.Surface(self.render_size).convert()
self.resolutions = resolutions
self.set_scale()
self.caption = caption
self.done = False
self.clock = pg.time.Clock()
self.fps = 60.0
self.show_fps = False
self.now = 0.0
self.keys = pg.key.get_pressed()
self.state_dict = OrderedDict()
self.state_name = None
self.state = None
self.music_handler = None
self.max_iterations = None
self.iterations = 0

self._disk_stats_available = False

def auto_discovery(self, scene_folder=None):
""" Scan a folder, load states found in it, and register them
:param scene_folder:
:return:
"""
if scene_folder is None:
scene_folder = os.path.join(".", "data", "states")

exclude_endings = (".py", ".pyc", "__pycache__")
for folder in os.listdir(scene_folder):
if any(folder.endswith(end) for end in exclude_endings):
continue

state = self.load_state_from_path(folder)
self.register_state(state)

def register_state(self, state):
try:
name = state.name
except AttributeError:
print('state {} is missing class name'.format(state))
raise AttributeError

if name in self.state_dict:
print('Duplicate state detected: {}'.format(name))
raise RuntimeError

self.state_dict[name] = state

@property
def saved_stats_are_available(self):
return self._disk_stats_available

def check_for_saved_stats(self):
""" TODO: this needs to not load the stats
:return: bool
"""
self._disk_stats_available = self.read_games_stats_from_disk() is not None

@staticmethod
def load_state_from_path(folder):
""" Load a state from disk, but do not register it
:param path: folder to load from
:return: Instanced state
"""
# TODO: not hardcode package name
package = "data.states."
try:
scene_module = import_module(package + folder)
state = scene_module.Scene
return state
except Exception as e:
template = "{} failed to load or is not a valid game package"
print(e)
print(template.format(folder))
raise

@staticmethod
def read_games_stats_from_disk():
""" Load stats saved in JSON format
:return: stats dict
"""
try:
path = os.path.join("resources", "save_game.json")
with open(path) as saved_file:
stats = json.load(saved_file)
except (IOError, ValueError):
stats = None

return stats

def create_new_games_stats(self):
""" Create new dict suitable for use when creating CasinoPlayer
Dict will contain all stats from any game that logs stats
:return: OrderedDict
"""
stats = dict()
stats['cash'] = prepare.MONEY
stats['account balance'] = 0
for name, state in self.state_dict.items():
func = getattr(state, 'initialize_stats', None)
if func:
game_stats = func()
stats[name] = game_stats

return stats

@staticmethod
def create_persist_from_stats(stats):
persist = OrderedDict()
persist['casino_player'] = CasinoPlayer(stats)
return persist

def create_new_persist(self):
""" Create new stats dictionary suitable for use as state persist
Default persist for all states will be new
Stats will be default from the states
:return: None
"""
stats = self.create_new_games_stats()
return self.create_persist_from_stats(stats)

def load_persist_from_disk(self):
""" Read stats from disk and set as default for new states
:return:
"""
stats = self.read_games_stats_from_disk()
return self.create_persist_from_stats(stats)

def start_state(self, state_name, persist=None):
""" Start a state
New stats will be created if there are none.
:param state_name: name of state to start
:param persist: dictionary of data for state
:return: None
"""
if persist is None:
persist = self.create_new_persist()

try:
state = self.state_dict[state_name]
except KeyError:
print('Cannot find state: {}'.format(state_name))
raise RuntimeError

instance = state()
instance.controller = self
instance.startup(self.now, persist)

self.state = instance
self.state_name = state_name

def query_all_states(self):
""" Return a dictionary of all loaded states
Keys are state names, values are State classes
:return: dictionary of all loaded states
"""
return self.state_dict.copy()

def update(self, dt):
"""
Checks if a state is done or has called for a game quit.
State is flipped if neccessary and State.update is called.
"""
self.screen = pg.display.get_surface()
self.now = pg.time.get_ticks()
if self.state.quit:
self.done = True
elif self.state.done:
self.flip_state()
self.state.update(self.render_surf,self.keys,self.now,dt,self.scale)
if self.music_handler and self.state.use_music_handler:
self.music_handler.update(self.scale)
self.music_handler.draw(self.render_surf)

def render(self):
"""
Scale the render surface if not the same size as the display surface.
The render surface is then drawn to the screen.
"""
if self.render_size != self.screen_rect.size:
scale_args = (self.render_surf, self.screen_rect.size, self.screen)
pg.transform.smoothscale(*scale_args)
else:
self.screen.blit(self.render_surf, (0, 0))

def flip_state(self):
"""
When a State changes to done necessary startup and cleanup functions
are called and the current State is changed.
"""
previous, self.state_name = self.state_name, self.state.next
persist = self.state.cleanup()
self.start_state(self.state_name, persist)
self.state.previous = previous

def event_loop(self):
"""
Process all events and pass them down to current State.
The f5 key globally turns on/off the display of FPS in the caption
"""
for event in pg.event.get():
if event.type == pg.KEYDOWN:
self.keys = pg.key.get_pressed()
self.toggle_show_fps(event.key)
if event.key == pg.K_PRINT:
#Print screen for full render-sized screencaps.
pg.image.save(self.render_surf, "screenshot.png")
elif event.type == pg.KEYUP:
self.keys = pg.key.get_pressed()
elif event.type == pg.VIDEORESIZE:
self.on_resize(event.size)
pg.event.clear(pg.VIDEORESIZE)
self.state.get_event(event, self.scale)
if self.music_handler and self.state.use_music_handler:
self.music_handler.get_event(event, self.scale)

def on_resize(self, size):
"""
If the user resized the window, change to the next available
resolution depending on if scaled up or scaled down.
"""
if size == self.screen_rect.size:
return
res_index = self.resolutions.index(self.screen_rect.size)
adjust = 1 if size > self.screen_rect.size else -1
if 0 <= res_index+adjust < len(self.resolutions):
new_size = self.resolutions[res_index+adjust]
else:
new_size = self.screen_rect.size
self.screen = pg.display.set_mode(new_size, pg.RESIZABLE)
self.screen_rect.size = new_size
self.set_scale()

def set_scale(self):
"""
Reset the ratio of render size to window size.
Used to make sure that mouse clicks are accurate on all resolutions.
"""
w_ratio = self.render_size[0]/float(self.screen_rect.w)
h_ratio = self.render_size[1]/float(self.screen_rect.h)
self.scale = (w_ratio, h_ratio)

def toggle_show_fps(self, key):
"""Press f5 to turn on/off displaying the framerate in the caption."""
if key == pg.K_F5:
self.show_fps = not self.show_fps
if not self.show_fps:
pg.display.set_caption(self.caption)

def main(self):
"""Main loop for entire program."""

self.check_for_saved_stats()

self.iterations = 0
while not self.is_complete():
time_delta = self.clock.tick(self.fps)
self.event_loop()
self.update(time_delta)
self.render()
pg.display.update()
if self.show_fps:
fps = self.clock.get_fps()
with_fps = "{} - {:.2f} FPS".format(self.caption, fps)
pg.display.set_caption(with_fps)
self.iterations += 1

def is_complete(self):
"""Return True if the control is complete and should stop running"""
if self.max_iterations is None:
return self.done
else:
return self.done or self.iterations >= self.max_iterations
Loading

0 comments on commit c562a67

Please sign in to comment.