HowTo Project: LifeGame

Objective

This howto will help you to discover synergine with a simple implementation. We will only show you the code of main logic. But all not displayed code here is described in main documentation.

The howto project implement the “Conway’s Game of Life”.

From Wikipedia, the free encyclopedia:

The Game of Life, also known simply as Life, is a cellular automaton devised by
the British mathematician John Horton Conway in 1970.

The "game" is a zero-player game, meaning that its evolution is determined by
its initial state, requiring no further input. One interacts with the Game of
Life by creating an initial configuration and observing how it evolves or, for
advanced players, by creating patterns with particular properties.

First steps

Note

The LifeGame howto source code is available at https://github.com/buxx/synergine_lifegame.git.

You will need some dependencies:

>>> pip install synergine
>>> pip install synergine_xyz

Content of simulation

Our simulation will implement a basic pattern of “Conway’s Game of Life” and visualisation tools.

Simulation

The objective is to implement the rules of “Conway’s Game of Life”. From Wikipedia, the free encyclopedia:

The universe of the Game of Life is an infinite two-dimensional orthogonal
grid of square cells, each of which is in one of two possible states, alive
or dead. Every cell interacts with its eight neighbours, which are the
cells that are horizontally, vertically, or diagonally adjacent. At each
step in time, the following transitions occur:

1. Any live cell with fewer than two live neighbours dies, as if caused by
   under-population.
2. Any live cell with two or three live neighbours lives on to the next generation.
3. Any live cell with more than three live neighbours dies, as if by overcrowding.
4. Any dead cell with exactly three live neighbours becomes a live cell, as if by
   reproduction.

The initial pattern constitutes the seed of the system. The first generation
is created by applying the above rules simultaneously to every cell in the
seed—births and deaths occur simultaneously, and the discrete moment at which
this happens is sometimes called a tick (in other words, each generation is a
pure function of the preceding one). The rules continue to be applied repeatedly
to create further generations.

Additional features

In additional of “Conway’s Game of Life” rules, we will represent differently in our 2d graphic output:

  • A just born cell
  • A born cell since 2 cycles
  • A born cell since more 2 cycles

Prerequisite

To correctly follow this tutorial, remember to work with Python 3.4 interpreter and create in your project directory a synergine_lifegame (sources files will be placed in) directory. Different command are executed from your project directory.

Constants

First, we have to prepare some constants to feed our metas data. There it is:

synergine_lifegame/cst.py:

from synergine.lib.eint import IncrementedNamedInt

ALIVE = IncrementedNamedInt.get('synergine_lifegame.alive')
DIED = IncrementedNamedInt.get('synergine_lifegame.died')

COL_DIED = IncrementedNamedInt.get('synergine_lifegame.col.died')
COL_ALIVE = IncrementedNamedInt.get('synergine_lifegame.col.alive')

SynergyObjects

We need: A Cell.

synergine_lifegame/synergy/object/Cell.py:

from synergine.cst import COL_ALL
from synergine_lifegame.cst import DIED, ALIVE, COL_DIED, COL_ALIVE
from synergine_xyz.SynergyObject import SynergyObject as XyzSynergyObject


class Cell(XyzSynergyObject):
    """
    Representation of cell.
    """

    def __init__(self, collection, context):
        """

        :param collection: Collection who contain this Cell
        :param context: The context object
        """
        super().__init__(collection, context)
        self._alive = False
        self._alive_since = -1
        # By default, a cell is dead
        self._add_col(COL_DIED)
        self._add_state(DIED)

    def set_alive(self, alive):
        """

        Change the state of Cell.

        :param alive: Alive boolean
        :return:
        """

        #  When alive state of Cell change, self._alive is reinitialized
        self._alive_since = -1
        self._alive = alive

        # We update states to.
        if alive:
            # State of cell is now ALIVE
            self._add_state(ALIVE)
            self._remove_state(DIED)
            # Cell is now in COL_ALIVE collection
            self._add_col(COL_ALIVE)
            self._remove_col(COL_DIED)
        else:
            # State of cell is now DIED
            self._add_state(DIED)
            self._remove_state(ALIVE)
            # Cell is now in COL_DIED collection
            self._add_col(COL_DIED)
            self._remove_col(COL_ALIVE)

    def is_alive(self):
        return self._alive

    def get_is_alive_since(self):
        """

        :return: Count of alive cycles.
        :rtype: int
        """
        return self._alive_since

    def end_cycle(self):
        """

        At each cycle, cell is more older if it's alive.

        :return:
        """
        if self.is_alive():
            self._alive_since += 1

Note

Cell class is a child class of synergine_xyz.SynergyObject.SynergyObject.

And no more for our SynergyObjects.

Events and Actions

AliveAroundEvent

Events/Actions will be “born” and “die”. These actions will need to know how many alive cells are around the concerned cell. So we write AliveAroundEvent event:

synergine_lifegame/synergy/event/AliveAroundEvent.py:

from synergine.core.exceptions import NotConcernedEvent
from synergine_xyz.mechanism.AroundMechanism import AroundMechanism
from synergine.synergy.event.ContactEvent import ContactEvent
from synergine_lifegame.cst import ALIVE, COL_DIED


class AliveAroundEvent(ContactEvent):
    """
    This class is a refactored class for die and born events.
    """

    def _get_alive_cell_around_count(self, context, parameters):
        cell_near_count = 0
        #  parameters dict is prepared by mechanism
        for object_id_near in parameters['objects_ids_near']:
            if context.metas.states.have(object_id_near, ALIVE):
                cell_near_count += 1
        return cell_near_count

Born and die event will be child of this event.

Born

The born event:

synergine_lifegame/synergy/event/GoodConditionToBornEvent.py:

from synergine_lifegame.synergy.event.AliveAroundEvent import AliveAroundEvent
from synergine.core.exceptions import NotConcernedEvent
from synergine_xyz.mechanism.AroundMechanism import AroundMechanism
from synergine_lifegame.cst import ALIVE, COL_DIED


class GoodConditionToBornEvent(AliveAroundEvent):
    """
    This event is applied when born condition are here. So when exactly 3 alive cell are around of observed cell.
    """

    _mechanism = AroundMechanism
    """This event need to know what is around concerned cell. So we use AroundMechanism who give us list of around
    objects ids."""

    _concern = COL_DIED
    """This event only concern died cells."""

    def _prepare(self, object_id, context, parameters={}):
        """
        According to “Conway’s Game of Life”, event match if exactly 3 around cell are alive.
        """
        cell_near_count = self._get_alive_cell_around_count(context, parameters)
        if cell_near_count is 3:
            return parameters
        #  If event not match, we must raise an NotConcernedEvent.
        raise NotConcernedEvent()

And his action:

synergine_lifegame/synergy/event/BornAction.py:

from synergine.synergy.event.Action import Action
from synergine_lifegame.synergy.event.GoodConditionToBornEvent import GoodConditionToBornEvent


class BornAction(Action):
    """
    This action change state of Cell into alive.
    """

    _listen = GoodConditionToBornEvent
    """This action listen the GoodConditionToBornEvent"""

    def run(self, obj, context, synergy_manager):
        obj.set_alive(True)

Die

The die event:

synergine_lifegame/synergy/event/NotGoodConditionToPersistEvent.py:

from synergine_lifegame.synergy.event.AliveAroundEvent import AliveAroundEvent
from synergine.core.exceptions import NotConcernedEvent
from synergine_xyz.mechanism.AroundMechanism import AroundMechanism
from synergine_lifegame.cst import ALIVE, COL_ALIVE


class NotGoodConditionToPersistEvent(AliveAroundEvent):
    """
    This event is applied when die condition are here. So when less of 2 or more of 3 alive cell are around
    of observed cell.
    """

    _mechanism = AroundMechanism
    """This event need to know what is around concerned cell. So we use AroundMechanism who give us list of around
    objects ids."""

    _concern = COL_ALIVE
    """This event only concern alive cells."""

    def _prepare(self, object_id, context, parameters={}):
        """
        According to “Conway’s Game of Life”, event match if less of 2 or more of 3 alive cell are around.
        """
        cell_near_count = self._get_alive_cell_around_count(context, parameters)
        if cell_near_count < 2 or cell_near_count > 3:
            return parameters
        #  If event not match, we must raise an NotConcernedEvent.
        raise NotConcernedEvent()

And his action:

synergine_lifegame/synergy/event/DieAction.py:

from synergine.synergy.event.Action import Action
from synergine_lifegame.synergy.event.NotGoodConditionToPersistEvent import NotGoodConditionToPersistEvent


class DieAction(Action):
    """
    This action change state of Cell into died.
    """

    _listen = NotGoodConditionToPersistEvent
    """This action listen the NotGoodConditionToPersistEvent"""

    def run(self, obj, context, synergy_manager):
        obj.set_alive(False)

AroundMechanism

As you can see, these events uses the AroundMechanism. This mechanism prepare a list of object ids who are around the observed object.

Collection

Our Cells must be contained by a Collection.

synergine_lifegame/synergy/collection/LifeGameCollection.py:

from synergine.synergy.collection.SynergyCollection import SynergyCollection
from synergine_lifegame.synergy.event.DieAction import DieAction
from synergine_lifegame.synergy.event.BornAction import BornAction
from synergine_lifegame.synergy.event.TimePassAction import TimePassAction


class LifeGameCollection(SynergyCollection):
    """
    This collection own cells of simulation.
    """

    def __init__(self, configuration):
        super().__init__(configuration)
        # We list here actions who concern our simulation.
        self._actions = [DieAction, BornAction, TimePassAction]

The collection must have a configuration (to populate his synergies objects).

synergine_lifegame/synergy/collection/LifeGameCollectionConfiguration.py:

from synergine.synergy.collection.Configuration import Configuration
from synergine_lifegame.synergy.object.Cell import Cell


class LifeGameCollectionConfiguration(Configuration):

    def get_start_objects(self, collection, context):
        cells = []

        # We build a grid of 40x50 cells
        for x in range(40):
            for y in range(50):
                cell = Cell(collection, context)
                cell.set_position((0, x, y))
                cells.append(cell)

        # We define some position for alive cells
        alive_cell_positions = (
            (0, 20, 20),
            (0, 21, 20),
            (0, 22, 20),
            (0, 22, 21),
            (0, 22, 22),
            (0, 21, 22),
            (0, 20, 22)
        )

        # And we born these alive cell
        for dead_cell in cells:
            if dead_cell.get_position() in alive_cell_positions:
                dead_cell.set_alive(True)

        # Synergy objects can be return to the collection
        return cells

And a Simulation container.

synergine_lifegame/synergy/LifeGameSimulation.py:

from synergine.synergy.Simulation import Simulation


class LifeGameSimulation(Simulation):
    """
    Life game simulation container.
    """
    pass  # Nothing to do here

Additional features

According to our additional features:

In additional of "Conway's Game of Life" rules, we will represent differently in our 2d graphic output:

* A just born cell
* A born cell since 2 cycles
* A born cell since more 2 cycles

We must add an Action who have to increment age of alive cells:

synergine_lifegame/synergy/event/TimePassAction.py:

from synergine.synergy.event.Action import Action
from synergine_lifegame.synergy.event.TimePassEvent import TimePassEvent
from synergine_lifegame.synergy.event.DieAction import DieAction
from synergine_lifegame.synergy.event.BornAction import BornAction


class TimePassAction(Action):

    _listen = TimePassEvent
    """This action listen the TimePassEvent"""
    _depend = [BornAction, DieAction]
    """This action need to be executed after Born and Die action"""

    def run(self, obj, context, synergy_manager):
        obj.end_cycle()

Who listen a simple event:

synergine_lifegame/synergy/event/TimePassEvent.py:

from synergine.synergy.event.Event import Event


class TimePassEvent(Event):

    def _prepare(self, obj, context, parameters={}):
        # All cells are concerned
        return parameters

Simple terminal

All needed algorithms are here. To be able to see something, we write a very simple output like this:

synergine_lifegame/PrintTerminal.py:

from synergine.core.connection.Terminal import Terminal


class PrintTerminal(Terminal):
    """
    A very simple Terminal to see how many cells are alive and dead.
    """

    def receive(self, actions_done):
        objects = self._synergy_manager.get_objects()
        alive_objects = [obj for obj in objects if obj.is_alive()]
        died_objects = [obj for obj in objects if not obj.is_alive()]
        cycle = self._context.get_cycle()

        print("Cycle %d: %d cells alive, %d cells died." % (cycle, len(alive_objects), len(died_objects)))

Let’s go

We now need to prepare configuration and run script for our simulation (run.py)

from synergine.core.Core import Core

from synergine_lifegame.PrintTerminal import PrintTerminal
from synergine_lifegame.synergy.collection.LifeGameCollection import LifeGameCollection
from synergine_lifegame.synergy.LifeGameSimulation import LifeGameSimulation
from synergine_lifegame.synergy.collection.LifeGameCollectionConfiguration import LifeGameCollectionConfiguration
from synergine_xyz.Context import Context as XyzContext

config = {
    'app': {
        'name': 'LifeGame simulation',
        'classes': {
            'Context': XyzContext
        }
    },
    'engine': {
        'fpsmax': 5,
    },
    'simulations': [LifeGameSimulation([LifeGameCollection(LifeGameCollectionConfiguration())])],
    'connections': [PrintTerminal]
}

if __name__ == '__main__':
    # Run simulation
    Core.start_core(config)

We execute script and, tadaaa:

python3.4 run.py
Cycle 0: 7 cells alive, 1993 dead cells.
Cycle 1: 7 cells alive, 1993 dead cells.
Cycle 2: 9 cells alive, 1991 dead cells.
Cycle 3: 9 cells alive, 1991 dead cells.
Cycle 4: 10 cells alive, 1990 dead cells.
Cycle 5: 12 cells alive, 1988 dead cells.
Cycle 6: 11 cells alive, 1989 dead cells.
Cycle 7: 16 cells alive, 1984 dead cells.
Cycle 8: 15 cells alive, 1985 dead cells.
Cycle 9: 23 cells alive, 1977 dead cells.
Cycle 10: 20 cells alive, 1980 dead cells.

Outputs

You can create all terminals you want. Some example:

2D pygame

Note

To install PyGame for Python 3.x you can follow these steps:

Xyzworld module delivery a ready to use PygameDisplay. We just need to prepare a display configuration for it:

synergine_lifegame/display/pygame_visualisation.py:

from synergine_xyz.display.object.pygame.PygameImage import PygameImage
from synergine_lifegame.synergy.object.Cell import Cell
from os import getcwd

image_cell_full = PygameImage.from_filepath(getcwd()+'/synergine_lifegame/display/pygame/cha3.png')
image_cell_medium = PygameImage.from_filepath(getcwd()+'/synergine_lifegame/display/pygame/cha2.png')
image_cell_small = PygameImage.from_filepath(getcwd()+'/synergine_lifegame/display/pygame/cha.png')
image_cell_dead = PygameImage.from_filepath(getcwd()+'/synergine_lifegame/display/pygame/cha0.png')


def alive_render(cell, context):
    """

    :param cell: Cell
    :param context: Context object
    :return: The image render
    :rtype: PygameImage
    """
    if cell.is_alive():
        if cell.get_is_alive_since() < 1:
            return image_cell_small
        elif cell.get_is_alive_since() == 1:
            return image_cell_medium
        else:
            return image_cell_full
    else:
        return image_cell_dead

visualisation = {
    'objects': {
        Cell: {
            'default': image_cell_small,
            'callbacks': [alive_render]
        }
    }
}

And add it to run configuration (run.py)

from lifegame.synergy.LifeGameSimulation import LifeGameSimulation
from lifegame.synergy.collection.LifeGameCollectionConfiguration import LifeGameCollectionConfiguration
from synergine_xyz.Context import Context as XyzContext
+from synergine_xyz.display.PygameDisplay import PygameDisplay
+from synergine_lifegame.display.pygame_visualisation import visualisation as pygame_visualisation

# ...

'simulations': [LifeGameSimulation([LifeGameCollection(LifeGameCollectionConfiguration())])],
-    'connections': [PrintTerminal]
+    'connections': [PrintTerminal, PygameDisplay],
+    'terminal': {
+        'pygame': {
+            'visualisation': pygame_visualisation,
+            'window_size': (1024, 768),
+            'display': {
+                'grid': {
+                    'size': 20
+                }
+            }
+        },
+    }
}

if __name__ == '__main__'

Then unzip these images into synergine_lifegame/display/pygame/ folder.

When we run our simulation we can now see a beautiful 2D render :

_images/synergine_lifegame_pygame.gif

LifeGame simulation capture of 2D pygame output.

Plot

Note

To get matplotlib and scipy dependencies on debian-like system you can execute:

>>> sudo apt-get install python3-scipy python3-matplotlib

Then, install matplotlib with pip with:

>>> pip install matplotlib

For example, we want to display a plot with history of alive cells count. We create a new Terminal:

synergine_lifegame/PlotTerminal.py:

from synergine.core.connection.Terminal import Terminal
import matplotlib.pyplot as plt


class PlotTerminal(Terminal):
    """
    A very simple Terminal to see how many cells are alive in plot.
    """

    def __init__(self, config, context, synergy_manager):
        super().__init__(config, context, synergy_manager)
        plt.axis([0, 1, 0, 1])
        plt.ion()
        plt.show()
        self._max_alive = 0

    def receive(self, actions_done):
        objects = self._synergy_manager.get_objects()
        alive_objects = [obj for obj in objects if obj.is_alive()]
        cycle = self._context.get_cycle()

        y = len(alive_objects)

        if y > self._max_alive:
            self._max_alive = y

        plt.axis([0, cycle, 0, self._max_alive])
        plt.scatter(cycle, y)
        plt.draw()

And add it to run configuration (run.py)

from lifegame.synergy.collection.LifeGameCollectionConfiguration import LifeGameCollectionConfiguration
from synergine_xyz.Context import Context as XyzContext
from synergine_xyz.display.PygameDisplay import PygameDisplay
+from synergine_lifegame.PlotTerminal import PlotTerminal

# ...

-    'connections': [PrintTerminal, PygameDisplay]
+    'connections': [PrintTerminal, PygameDisplay, PlotTerminal],

And let see result:

_images/synergine_lifegame_plot.gif

Note

If matplotlib don’t display, it’s possibly pip bugged package. Try with OS package python-matplotlib.

You are now ready to use synergine. You can visit intelligine project to see advanced usage of synergine.