removed package dependency on pygame and added test for display

This commit is contained in:
Richard Feistenauer 2019-04-14 11:20:08 +02:00
parent 1a96e90116
commit 9f447b4a05
9 changed files with 193 additions and 49 deletions

View File

@ -97,8 +97,11 @@ There ist still quite some work to do.
And for all others, don't hesitate to open issues when you have problems!
## Dependencies
As mentioned above the module depends on [pygame](https://www.pygame.org/news) for visualisation.
This is the only dependency however.
For direct usage of the cellular automaton ther is no dependency.
If you want to use the display option however or execute the examples you will have to install
[pygame](https://www.pygame.org/news) for visualisation.
If you do for some reason not want to use this engine simply inherit from display.DrawEngine and overwrite the
necessary methods. (for an example of how to do so see ./test/test_display.py)
## Licence
This package is distributed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0), see [LICENSE.txt](./LICENSE.txt)

1
__init__.py Normal file
View File

@ -0,0 +1 @@
from cellular_automaton.cellular_automaton import *

View File

@ -14,19 +14,90 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
import pygame
import time
import operator
from . import automaton
class _Rect:
def __init__(self, left=0, top=0, width=0, height=0, rect=None, pos=None, size=None):
if rect is not None and (pos is not None or size is not None):
raise ValueError("define either rect OR position and size OR left, top, width and height")
if not (left == top == width == height == 0) and not(rect == pos == size is None):
raise ValueError("define either rect OR position and size OR left, top, width and height")
self.__direct_initialisation(height, left, top, width)
self.__pos_and_size_initialisation(pos, size)
self.__rect_initialisation(rect)
def __rect_initialisation(self, rect):
if rect is not None:
self.__direct_initialisation(rect[1][1], rect[0][0], rect[0][1], rect[1][0])
def __pos_and_size_initialisation(self, pos, size):
if pos is not None:
self.left = pos[0]
self.top = pos[1]
if size is not None:
self.width = size[0]
self.height = size[1]
def __direct_initialisation(self, height, left, top, width):
self.left = left
self.top = top
self.width = width
self.height = height
def get_rect_tuple(self):
return (self.left, self.top), (self.width, self.height)
class DrawEngine(object):
def __init__(self, window_size=None, *args, **kwargs):
super().__init__(*args, **kwargs)
global pygame
import pygame
pygame.init()
pygame.display.set_caption("Cellular Automaton")
self.__screen = pygame.display.set_mode(window_size)
self.__font = pygame.font.SysFont("monospace", 15)
self._width = window_size[0]
self._height = window_size[1]
def write_text(self, pos, text, color=(0, 255, 0)):
label = self.__font.render(text, 1, color)
update_rect = self.__screen.blit(label, pos)
DrawEngine.update_rectangles(update_rect)
def fill_surface_with_color(self, rect, color=(0, 0, 0)):
return self.__screen.fill(color, rect)
@staticmethod
def update_rectangles(rectangles):
pygame.display.update(rectangles)
@staticmethod
def is_active():
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
return True
class _CASurface:
def __init__(self, grid_rect, cellular_automaton: automaton.CellularAutomatonProcessor, screen):
def __init__(self,
grid_rect,
cellular_automaton: automaton.CellularAutomatonProcessor,
draw_engine,
*args, **kwargs):
super().__init__(*args, **kwargs)
self._cellular_automaton = cellular_automaton
self.__rect = grid_rect
self.__cell_size = self._calculate_cell_display_size()
self.__screen = screen
self.__draw_engine = draw_engine
def _calculate_cell_display_size(self):
grid_dimension = self._cellular_automaton.get_dimension()
@ -34,8 +105,7 @@ class _CASurface:
def redraw_cellular_automaton(self):
""" Redraws those cells which changed their state since last redraw. """
update_rectangles = list(self.__redraw_dirty_cells())
pygame.display.update(update_rectangles)
self.__draw_engine.update_rectangles(list(self.__redraw_dirty_cells()))
def __redraw_dirty_cells(self):
for coordinate, cell in self._cellular_automaton.get_cells().items():
@ -47,7 +117,7 @@ class _CASurface:
cell_pos = self._calculate_cell_position_in_the_grid(coordinate)
surface_pos = self._calculate_cell_position_on_screen(cell_pos)
cell.was_redrawn()
yield self._draw_the_cell_to_screen(cell_color, surface_pos)
yield self._draw_cell_surface(surface_pos, cell_color)
def __get_cell_color(self, cell):
return self._cellular_automaton.get_current_rule().get_state_draw_color(
@ -59,33 +129,25 @@ class _CASurface:
def _calculate_cell_position_on_screen(self, cell_pos):
return [self.__rect.left + cell_pos[0], self.__rect.top + cell_pos[1]]
def _draw_the_cell_to_screen(self, cell_color, surface_pos):
return self.__screen.fill(cell_color, (surface_pos, self.__cell_size))
def _draw_cell_surface(self, surface_pos, cell_color):
return self.__draw_engine.fill_surface_with_color((surface_pos, self.__cell_size), cell_color)
class CAWindow:
class CAWindow(DrawEngine):
def __init__(self, cellular_automaton: automaton.CellularAutomatonProcessor,
evolution_steps_per_draw=1,
window_size=(1000, 800)):
window_size=(1000, 800),
*args, **kwargs):
super().__init__(window_size=window_size, *args, **kwargs)
self._ca = cellular_automaton
self.__window_size = window_size
self.__init_pygame()
self.ca_display = _CASurface(_Rect(pos=(0, 30), size=(window_size[0], window_size[1] - 30)),
self._ca,
self)
self.__loop_evolution_and_redraw_of_automaton(evolution_steps_per_draw=evolution_steps_per_draw)
def __init_pygame(self):
pygame.init()
pygame.display.set_caption("Cellular Automaton")
self._screen = pygame.display.set_mode(self.__window_size)
self._font = pygame.font.SysFont("monospace", 15)
self.ca_display = _CASurface(pygame.Rect(0, 30, self.__window_size[0], self.__window_size[1] - 30),
self._ca,
self._screen)
def __loop_evolution_and_redraw_of_automaton(self, evolution_steps_per_draw):
running = True
while running:
while super().is_active():
time_ca_start = time.time()
self._ca.evolve_x_times(evolution_steps_per_draw)
time_ca_end = time.time()
@ -93,17 +155,8 @@ class CAWindow:
time_ds_end = time.time()
self.__print_process_duration(time_ca_end, time_ca_start, time_ds_end)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
def __print_process_duration(self, time_ca_end, time_ca_start, time_ds_end):
self._screen.fill([0, 0, 0], ((0, 0), (self.__window_size[0], 30)))
self.__write_text((10, 5), "CA: " + "{0:.4f}".format(time_ca_end - time_ca_start) + "s")
self.__write_text((310, 5), "Display: " + "{0:.4f}".format(time_ds_end - time_ca_end) + "s")
self.__write_text((660, 5), "Step: " + str(self._ca.get_current_evolution_step()))
def __write_text(self, pos, text, color=(0, 255, 0)):
label = self._font.render(text, 1, color)
update_rect = self._screen.blit(label, pos)
pygame.display.update(update_rect)
super().fill_surface_with_color(_Rect(size=(self._width, 30)).get_rect_tuple())
super().write_text((10, 5), "CA: " + "{0:.4f}".format(time_ca_end - time_ca_start) + "s")
super().write_text((310, 5), "Display: " + "{0:.4f}".format(time_ds_end - time_ca_end) + "s")
super().write_text((660, 5), "Step: " + str(self._ca.get_current_evolution_step()))

View File

@ -14,6 +14,6 @@ setup(
description="N dimensional cellular automaton with multi processing capability.",
long_description=long_description,
long_description_content_type='text/markdown',
requires=["pygame"],
requires=[""],
python_requires='>3.6.1'
)

0
test/__init__.py Normal file
View File

View File

@ -15,10 +15,10 @@ limitations under the License.
"""
import sys
sys.path.append('../cellular_automaton')
sys.path.append('..')
from cellular_automaton.cell import Cell
from cellular_automaton.cell_state import CellState
from cellular_automaton.cellular_automaton.cell import Cell
from cellular_automaton.cellular_automaton.cell_state import CellState
import unittest

View File

@ -17,13 +17,13 @@ limitations under the License.
import sys
sys.path.append('../cellular_automaton')
from cellular_automaton import cell_state
from cellular_automaton.cellular_automaton.cell_state import SynchronousCellState
import unittest
class TestCellState(unittest.TestCase):
def setUp(self):
self.cell_state = cell_state.SynchronousCellState(initial_state=(0,), draw_first_state=False)
self.cell_state = SynchronousCellState(initial_state=(0,), draw_first_state=False)
def test_get_state_with_overflow(self):
self.cell_state.set_state_of_evolution_step(new_state=(1,), evolution_step=0)
@ -63,7 +63,7 @@ class TestCellState(unittest.TestCase):
return self.cell_state.set_state_of_evolution_step(new_state=(1, 1), evolution_step=0)
def test_redraw_flag(self):
self.cell_state = cell_state.SynchronousCellState(initial_state=(0,), draw_first_state=True)
self.cell_state = SynchronousCellState(initial_state=(0,), draw_first_state=True)
self.assertTrue(self.cell_state.is_set_for_redraw())
self.cell_state.was_redrawn()
self.assertFalse(self.cell_state.is_set_for_redraw())

87
test/test_display.py Normal file
View File

@ -0,0 +1,87 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
sys.path.append('../cellular_automaton')
from cellular_automaton import *
import unittest
class TestRule(Rule):
def init_state(self, cell_coordinate):
return [1] if cell_coordinate == (1, 1) else [0]
def evolve_cell(self, last_cell_state, neighbors_last_states):
return [last_cell_state[0] + 1] if neighbors_last_states else last_cell_state
def get_state_draw_color(self, current_state):
return 255, 0, 0
class DrawEngineMock(display.DrawEngine):
written_texts = 0
filled_surfaces = 0
updated_rectangles = 0
_draws = 0
_draws_until_end = 1
def __init__(self, window_size=None, draws_until_end=1):
super(display.DrawEngine, self).__init__()
self._width = window_size[0]
self._height = window_size[1]
DrawEngineMock.written_texts = 0
DrawEngineMock.filled_surfaces = 0
DrawEngineMock.updated_rectangles = 0
DrawEngineMock._draws = 0
DrawEngineMock._draws_until_end = draws_until_end
def write_text(self, pos, text, color=(0, 255, 0)):
self.written_texts += 1
def fill_surface_with_color(self, rect, color=(0, 0, 0)):
self.filled_surfaces += 1
@staticmethod
def update_rectangles(rectangles):
DrawEngineMock.updated_rectangles += len(rectangles)
DrawEngineMock._draws += 1
@staticmethod
def is_active():
return DrawEngineMock._draws < DrawEngineMock._draws_until_end
class CAWindowMock(CAWindow, DrawEngineMock):
""" Mocks the window with fake engine. """
class TestDisplay(unittest.TestCase):
def setUp(self):
self.ca = CAFactory.make_single_process_cellular_automaton((3, 3), MooreNeighborhood(), TestRule)
def test_evolution_steps_per_draw(self):
mock = CAWindowMock(self.ca, evolution_steps_per_draw=10, window_size=(10, 10))
self.assertEqual(self.ca.get_current_evolution_step(), 10)
def test_updated_rectangle_count(self):
mock = CAWindowMock(self.ca, evolution_steps_per_draw=1, window_size=(10, 10), draws_until_end=4)
self.assertEqual(DrawEngineMock.updated_rectangles, 9 + 3)
if __name__ == '__main__':
unittest.main()

View File

@ -18,8 +18,8 @@ import sys
sys.path.append('../cellular_automaton')
from cellular_automaton import *
from cellular_automaton.cell_state import CellState
from cellular_automaton.state import CellularAutomatonState
from cellular_automaton.cellular_automaton.cell_state import CellState
from cellular_automaton.cellular_automaton.state import CellularAutomatonState
import unittest
import mock