Skip to content

Commit 2591275

Browse files
committed
Refactor cellular automata screensavers
1) They now extend a base class: CellularAutomaton. Subclasses thus far as GameOfLife and CyclicAutomaton 2) Adds various configuration settings to sample_config.json for CyclicAutomaton 3) Creates a screensaver_manager process that the queue starts to manage screensaver playback.
1 parent 272b1dd commit 2591275

File tree

13 files changed

+545
-458
lines changed

13 files changed

+545
-458
lines changed

bin/cyclic_automaton

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
6+
# This is necessary for the imports below to work
7+
root_dir = os.path.abspath(os.path.dirname(__file__) + '/..')
8+
sys.path.append(root_dir)
9+
10+
import argparse
11+
from pifi.config import Config
12+
from pifi.games.cellularautomata.cyclicautomaton import CyclicAutomaton
13+
from pifi.logger import Logger
14+
15+
def parseArgs():
16+
parser = argparse.ArgumentParser(
17+
description=("Cyclic automaton: https://en.wikipedia.org/wiki/Cyclic_cellular_automaton ."),
18+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
19+
)
20+
parser.add_argument('--log-uuid', dest='log_uuid', action='store', help='Logger UUID')
21+
22+
args = parser.parse_args()
23+
return args
24+
25+
26+
args = parseArgs()
27+
if args.log_uuid:
28+
Logger.set_uuid(args.log_uuid)
29+
Config.load_config_if_not_loaded()
30+
31+
CyclicAutomaton().play()

bin/game_of_life

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,15 @@ sys.path.append(root_dir)
99

1010
import argparse
1111
from pifi.config import Config
12-
from pifi.games.gameoflife import GameOfLife
13-
from pifi.games.gamecolorhelper import GameColorHelper
12+
from pifi.games.cellularautomata.gameoflife import GameOfLife
1413
from pifi.logger import Logger
1514

1615
def parseArgs():
1716
parser = argparse.ArgumentParser(
18-
description=("Conway's game of life. Arguments that are not specified will get values from your " +
19-
"config file, if you have one."),
17+
description=("Conway's game of life."),
2018
formatter_class=argparse.ArgumentDefaultsHelpFormatter
2119
)
2220
parser.add_argument('--log-uuid', dest='log_uuid', action='store', help='Logger UUID')
23-
parser.add_argument('--brightness', dest='brightness', action='store', type=int,
24-
metavar='N', help='Global brightness value. Max of 31.')
25-
parser.add_argument('--loop', dest='should_loop', action='store_true', default=False,
26-
help='Whether to start a new game after game over.')
27-
parser.add_argument('--seed-liveness-probability', dest='seed_liveness_probability', action='store', type=float,
28-
metavar='N', help='Probability that each pixel is live when seeding.')
29-
parser.add_argument('--tick-sleep', dest='tick_sleep', action='store', type=float,
30-
metavar='N', help='Amount to sleep after each tick.')
31-
parser.add_argument('--game-over-detection-lookback', dest='game_over_detection_lookback', action='store', type=int,
32-
metavar='N', help='Number of turns to look back to see if game has not changed (i.e. it is over).')
33-
parser.add_argument('--fade', dest='fade', action='store_true', help='fade between each tick', default=None)
34-
parser.add_argument('--game-color-mode', dest='game_color_mode', action='store',
35-
help=(GameColorHelper().get_help_string()))
36-
parser.add_argument('--variant', dest='variant', action='store', help=f'One of {GameOfLife.VARIANTS}.')
3721

3822
args = parser.parse_args()
3923
return args
@@ -44,19 +28,4 @@ if args.log_uuid:
4428
Logger.set_uuid(args.log_uuid)
4529
Config.load_config_if_not_loaded()
4630

47-
if args.brightness is not None:
48-
Config.set('leds.brightness', args.brightness)
49-
if args.seed_liveness_probability is not None:
50-
Config.set('game_of_life.seed_liveness_probability', args.seed_liveness_probability)
51-
if args.tick_sleep is not None:
52-
Config.set('game_of_life.tick_sleep', args.tick_sleep)
53-
if args.game_over_detection_lookback is not None:
54-
Config.set('game_of_life.game_over_detection_lookback', args.game_over_detection_lookback)
55-
if args.game_color_mode is not None:
56-
Config.set('game_of_life.game_color_mode', args.game_color_mode)
57-
if args.fade is not None:
58-
Config.set('game_of_life.fade', args.fade)
59-
if args.variant is not None:
60-
Config.set('game_of_life.variant', args.variant)
61-
62-
GameOfLife().play(should_loop = args.should_loop)
31+
GameOfLife().play()

bin/screensaver_manager

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
6+
# This is necessary for the imports below to work
7+
root_dir = os.path.abspath(os.path.dirname(__file__) + '/..')
8+
sys.path.append(root_dir)
9+
10+
import argparse
11+
from pifi.config import Config
12+
from pifi.logger import Logger
13+
from pifi.screensavermanager import ScreensaverManager
14+
15+
def parseArgs():
16+
parser = argparse.ArgumentParser(
17+
description=("Plays screensavers that play while nothing is in the playlist queue."),
18+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
19+
)
20+
parser.add_argument('--log-uuid', dest='log_uuid', action='store', help='Logger UUID')
21+
22+
args = parser.parse_args()
23+
return args
24+
25+
26+
args = parseArgs()
27+
if args.log_uuid:
28+
Logger.set_uuid(args.log_uuid)
29+
Config.load_config_if_not_loaded()
30+
31+
ScreensaverManager().run()
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from abc import ABC, abstractmethod
2+
import hashlib
3+
import time
4+
5+
from pifi.logger import Logger
6+
from pifi.led.ledframeplayer import LedFramePlayer
7+
from pifi.datastructure.limitedsizedict import LimitedSizeDict
8+
9+
# Base class for cellular automaton games
10+
class CellularAutomaton(ABC):
11+
12+
_DEFAULT_MAX_GAME_LENGTH_SECONDS = 0 # unlimited
13+
_DEFAULT_MAX_STATE_REPETITIONS_FOR_GAME_OVER = 10
14+
_DEFAULT_TICK_SLEEP_SECONDS = 0.07
15+
_DEFAULT_GAME_OVER_LOOKBACK_DETECTION_AMOUNT = 16
16+
_DEFAULT_SHOULD_FADE_TO_FRAME = False
17+
18+
def __init__(self, led_frame_player = None):
19+
self._logger = Logger().set_namespace(self.__class__.__name__)
20+
self._board = None
21+
if led_frame_player is None:
22+
self.__led_frame_player = LedFramePlayer()
23+
else:
24+
self.__led_frame_player = led_frame_player
25+
26+
def play(self, should_loop = False):
27+
if should_loop:
28+
while True:
29+
self.tick(should_loop = should_loop)
30+
else:
31+
self.__reset()
32+
while True:
33+
if not self.tick():
34+
break
35+
36+
# return False when the game is over, True otherwise.
37+
def tick(self, should_loop = False, force_reset = False):
38+
is_game_in_progress = True
39+
if self._board is None or force_reset:
40+
# start the game
41+
self.__reset()
42+
else:
43+
self.__tick_internal()
44+
45+
self.__do_tick_bookkeeping()
46+
47+
if self.__is_game_over():
48+
if should_loop:
49+
self.__reset()
50+
self.__do_tick_bookkeeping()
51+
is_game_in_progress = True
52+
else:
53+
is_game_in_progress = False
54+
55+
time.sleep(self._get_tick_sleep_seconds())
56+
57+
return is_game_in_progress
58+
59+
def __tick_internal(self):
60+
self._update_board()
61+
self.__show_board()
62+
63+
def __do_tick_bookkeeping(self):
64+
self._num_ticks += 1
65+
board_hash = self.__get_board_hash()
66+
if board_hash in self.__prev_board_state_counts:
67+
self.__prev_board_state_counts[board_hash] += 1
68+
else:
69+
self.__prev_board_state_counts[board_hash] = 1
70+
71+
def __show_board(self):
72+
frame = self._board_to_frame()
73+
74+
if self._should_fade_to_frame():
75+
self.__led_frame_player.fade_to_frame(frame)
76+
else:
77+
self.__led_frame_player.play_frame(frame)
78+
79+
def __get_board_hash(self):
80+
return hashlib.md5(self._board).hexdigest()
81+
82+
def __is_game_over(self):
83+
if self._get_max_game_length_seconds() > 0 and (time.time() - self.__start_time) > self._get_max_game_length_seconds():
84+
self._logger.info("Game over due to timeout expiration.")
85+
return True
86+
87+
if self.__prev_board_state_counts[self.__get_board_hash()] > self._get_max_state_repetitions_for_game_over():
88+
self._logger.info("Game over detected. Current board state has repeated at least " +
89+
f"{self._get_max_state_repetitions_for_game_over()} times.")
90+
return True
91+
return False
92+
93+
def __reset(self):
94+
self._logger.info("Starting new game.")
95+
self.__start_time = time.time()
96+
self._num_ticks = 0
97+
self.__prev_board_state_counts = LimitedSizeDict(capacity = self._get_game_over_detection_lookback_amount())
98+
99+
self._reset_hook()
100+
101+
self.__seed()
102+
103+
def __seed(self):
104+
self._seed_hook()
105+
self.__show_board()
106+
107+
@abstractmethod
108+
def _reset_hook(self):
109+
pass
110+
111+
@abstractmethod
112+
def _seed_hook(self):
113+
pass
114+
115+
@abstractmethod
116+
def _board_to_frame(self):
117+
pass
118+
119+
@abstractmethod
120+
def _update_board(self):
121+
pass
122+
123+
def _get_tick_sleep_seconds(self):
124+
return self._DEFAULT_TICK_SLEEP_SECONDS
125+
126+
def _get_game_over_detection_lookback_amount(self):
127+
return self._DEFAULT_GAME_OVER_LOOKBACK_DETECTION_AMOUNT
128+
129+
def _should_fade_to_frame(self):
130+
return self._DEFAULT_SHOULD_FADE_TO_FRAME
131+
132+
def _get_max_state_repetitions_for_game_over(self):
133+
return self._DEFAULT_MAX_STATE_REPETITIONS_FOR_GAME_OVER
134+
135+
# 0 means unlimited
136+
def _get_max_game_length_seconds(self):
137+
return self._DEFAULT_MAX_GAME_LENGTH_SECONDS
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import numpy as np
2+
import random
3+
4+
from pifi.config import Config
5+
from pifi.games.cellularautomata.palette import palettes
6+
from pifi.games.cellularautomata.cellularautomaton import CellularAutomaton
7+
8+
# https://en.wikipedia.org/wiki/Cyclic_cellular_automaton
9+
class CyclicAutomaton(CellularAutomaton):
10+
11+
def _get_tick_sleep_seconds(self):
12+
return Config.get('cyclic_automaton.tick_sleep', self._DEFAULT_TICK_SLEEP_SECONDS)
13+
14+
def _get_game_over_detection_lookback_amount(self):
15+
return Config.get('cyclic_automaton.game_over_detection_lookback', self._DEFAULT_GAME_OVER_LOOKBACK_DETECTION_AMOUNT)
16+
17+
def _should_fade_to_frame(self):
18+
return Config.get('cyclic_automaton.fade', self._DEFAULT_SHOULD_FADE_TO_FRAME)
19+
20+
def _get_max_game_length_seconds(self):
21+
return 60
22+
23+
def _reset_hook(self):
24+
self.__num_states = random.randint(5, 16)
25+
self.__palette = np.array(random.choice(palettes))
26+
27+
def _seed_hook(self):
28+
# Create the board with an extra edge cell on all sides to simplify the
29+
# neighborhood calculation and avoid edge checks.
30+
shape = [Config.get_or_throw('leds.display_height') + 2, Config.get_or_throw('leds.display_width') + 2]
31+
self._board = np.random.randint(0, self.__num_states, shape)
32+
33+
def _board_to_frame(self):
34+
board_shape = self._board.shape + (3,)
35+
foo = self.__palette[self._board.ravel()].reshape(board_shape)
36+
return foo[1:-1, 1:-1]
37+
38+
def _update_board(self):
39+
b = self._board
40+
41+
slices = [((0,-2), (0,-2)),
42+
((0,-2), (1,-1)),
43+
((0,-2), (2,None)),
44+
((1,-1), (0,-2)),
45+
((1,-1), (2,None)),
46+
((2,None), (0,-2)),
47+
((2,None), (1,-1)),
48+
((2,None), (2,None))]
49+
50+
succ = None
51+
bpad = b[1:-1, 1:-1]
52+
for indices in slices:
53+
s = tuple(slice(*x) for x in indices)
54+
if succ is None:
55+
succ = (b[s] == ((bpad + 1) % self.__num_states))
56+
else:
57+
succ |= (b[s] == ((bpad + 1) % self.__num_states))
58+
59+
b[1:-1, 1:-1][succ] = ((bpad[succ] + 1) % self.__num_states)
60+
61+
self._board = b

0 commit comments

Comments
 (0)