Browse Source

XYZ dev and working life game bases

Bastien Sevajol 7 years ago
parent
commit
514181c0da

+ 0 - 0
sandbox/__init__.py View File


+ 1 - 0
sandbox/life_game/__init__.py View File

@@ -0,0 +1 @@
1
+__author__ = 'bux'

+ 58 - 0
sandbox/life_game/simulation.py View File

@@ -0,0 +1,58 @@
1
+from synergine2.simulation import Subject
2
+from synergine2.simulation import Behaviour
3
+from synergine2.xyz import ProximityMechanism
4
+from synergine2.xyz import XYZSubjectMixin
5
+
6
+COLLECTION_CELL = 'COLLECTION_CELL'  # Collections of Cell type
7
+
8
+
9
+class CellProximityMechanism(ProximityMechanism):
10
+    distance = 1.41  # distance when on angle
11
+    feel_collections = [COLLECTION_CELL]
12
+
13
+
14
+class CellDieBehaviour(Behaviour):
15
+    use = [CellProximityMechanism]
16
+
17
+    def run(self, data):
18
+        around_count = len(data[CellProximityMechanism])
19
+        if around_count in [2, 3]:
20
+            return False
21
+        return True  # If we return around_count, when around_count is 0
22
+
23
+    def action(self, data):
24
+        new_empty = Empty(
25
+            simulation=self.simulation,
26
+            position=self.subject.position,
27
+        )
28
+        self.simulation.subjects.remove(self.subject)
29
+        self.simulation.subjects.append(new_empty)
30
+
31
+
32
+class CellBornBehaviour(Behaviour):
33
+    use = [CellProximityMechanism]
34
+
35
+    def run(self, data):
36
+        around_count = len(data[CellProximityMechanism])
37
+        if around_count == 3:
38
+            return 3
39
+        return False
40
+
41
+    def action(self, data):
42
+        new_cell = Cell(
43
+            simulation=self.simulation,
44
+            position=self.subject.position,
45
+        )
46
+        self.simulation.subjects.remove(self.subject)
47
+        self.simulation.subjects.append(new_cell)
48
+
49
+
50
+class Cell(XYZSubjectMixin, Subject):
51
+    collections = Subject.collections[:]
52
+    collections.extend([COLLECTION_CELL])
53
+    behaviours_classes = [CellDieBehaviour]
54
+
55
+
56
+class Empty(XYZSubjectMixin, Subject):
57
+    """Represent empty position where cell can spawn"""
58
+    behaviours_classes = [CellBornBehaviour]

+ 66 - 0
synergine2/cycle.py View File

@@ -0,0 +1,66 @@
1
+import multiprocessing
2
+
3
+from synergine2.processing import ProcessManager
4
+from synergine2.simulation import Subject, Behaviour, Mechanism
5
+from synergine2.utils import ChunkManager
6
+
7
+
8
+class CycleManager(object):
9
+    def __init__(
10
+            self,
11
+            subjects: list,
12
+            process_manager: ProcessManager=None,
13
+    ):
14
+        if process_manager is None:
15
+            process_manager = ProcessManager(
16
+                process_count=multiprocessing.cpu_count(),
17
+                chunk_manager=ChunkManager(multiprocessing.cpu_count()),
18
+                job_maker=self.computing,
19
+            )
20
+
21
+        self.subjects = subjects
22
+        self._process_manager = process_manager
23
+        self._current_cycle = 0
24
+
25
+    def next(self):
26
+        results = {}
27
+        results_by_processes = self._process_manager.execute_jobs(self.subjects)
28
+        for process_results in results_by_processes:
29
+            results.update(process_results)
30
+        for subject in self.subjects[:]:  # Duplicate list to prevent conflicts with behaviours subjects manipulations
31
+            for behaviour_class in results[subject.id]:
32
+                # TODO: Ajouter une etape de selection des actions a faire (genre neuronnal)
33
+                # TODO: les behaviour_class ont le même uniqueid apres le process ?
34
+                subject.behaviours[behaviour_class].action(results[subject.id][behaviour_class])
35
+
36
+    def computing(self, subjects):
37
+        # compute mechanisms (prepare check to not compute slienced or not used mechanisms)
38
+        # compute behaviours with mechanisms data
39
+        # return list with per subject: [behaviour: return, behaviour: return] if behaviour return True something
40
+        results = {}
41
+        for subject in subjects:
42
+            mechanisms = self.get_mechanisms_to_compute(subject)
43
+            mechanisms_data = {}
44
+            behaviours_data = {}
45
+
46
+            for mechanism in mechanisms:
47
+                mechanisms_data[type(mechanism)] = mechanism.run()
48
+
49
+            for behaviour in self.get_behaviours_to_compute(subject):
50
+                # We identify behaviour data with it's class to be able to intersect it after subprocess data collect
51
+                behaviour_data = behaviour.run(mechanisms_data)
52
+                if behaviour_data:
53
+                    behaviours_data[type(behaviour)] = behaviour_data
54
+
55
+            results[subject.id] = behaviours_data
56
+            # TODO: Tester les performances si on utilise un index numerique pour les results[subject]
57
+            # et behaviours_data[type(behaviours_data)]
58
+        return results
59
+
60
+    def get_mechanisms_to_compute(self, subject: Subject) -> [Mechanism]:
61
+        # TODO: Implementer un systeme qui inhibe des mechanisme (ex. someil inhibe l'ouie)
62
+        return subject.mechanisms.values()
63
+
64
+    def get_behaviours_to_compute(self, subject: Subject) -> [Behaviour]:
65
+        # TODO: Implementer un systeme qui inhibe des behaviours (ex. someil inhibe avoir faim)
66
+        return subject.behaviours.values()

+ 89 - 0
synergine2/simulation.py View File

@@ -0,0 +1,89 @@
1
+import collections
2
+
3
+from synergine2.utils import initialize_subject
4
+
5
+
6
+class Simulation(object):
7
+    def __init__(self):
8
+        self.collections = collections.defaultdict(list)
9
+        self._subjects = None
10
+
11
+    @property
12
+    def subjects(self):
13
+        return self._subjects
14
+
15
+    @subjects.setter
16
+    def subjects(self, value: 'Subjects'):
17
+        if not isinstance(value, Subjects):
18
+            raise Exception('Simulation.subjects must be Subjects type')
19
+        self._subjects = value
20
+
21
+
22
+class Subject(object):
23
+    collections = []
24
+    behaviours_classes = []
25
+
26
+    def __init__(self, simulation: Simulation):
27
+        self.id = id(self)  # We store object id because it's lost between process
28
+        self.simulation = simulation
29
+        self.behaviours = {}
30
+        self.mechanisms = {}
31
+
32
+        for collection in self.collections:
33
+            self.simulation.collections[collection].append(self)
34
+
35
+        initialize_subject(
36
+            simulation=simulation,
37
+            subject=self,
38
+        )
39
+
40
+
41
+class Subjects(list):
42
+    def __init__(self, *args, **kwargs):
43
+        self.simulation = kwargs.pop('simulation')
44
+        super().__init__(*args, **kwargs)
45
+
46
+    def remove(self, value: Subject):
47
+        super().remove(value)
48
+        for collection_name in value.collections:
49
+            self.simulation.collections[collection_name].remove(value)
50
+
51
+
52
+class Mechanism(object):
53
+    def __init__(
54
+            self,
55
+            simulation: Simulation,
56
+            subject: Subject,
57
+    ):
58
+        self.simulation = simulation
59
+        self.subject = subject
60
+
61
+    def run(self):
62
+        raise NotImplementedError()
63
+
64
+
65
+class Behaviour(object):
66
+    def __init__(
67
+            self,
68
+            simulation: Simulation,
69
+            subject: Subject,
70
+    ):
71
+        self.simulation = simulation
72
+        self.subject = subject
73
+
74
+    def run(self, data):
75
+        """
76
+        Method called in subprocess.
77
+        If return equivalent to False, behaviour produce nothing.
78
+        If return equivalent to True, action bahaviour method
79
+        will be called with these data
80
+        Note: Returned data will be transfered from sub processes.
81
+              Prefer scalar types.
82
+        """
83
+        raise NotImplementedError()
84
+
85
+    def action(self, data):
86
+        """
87
+        Method called in main process
88
+        """
89
+        raise NotImplementedError()

+ 24 - 0
synergine2/utils.py View File

@@ -10,3 +10,27 @@ class ChunkManager(object):
10 10
             a, j = j, j + (i + k) // self._chunks_numbers
11 11
             x.append(data[a:j])
12 12
         return x
13
+
14
+
15
+def get_mechanisms_classes(subject: 'Subject') -> ['Mechanisms']:
16
+    mechanisms_classes = []
17
+    for behaviour_class in subject.behaviours_classes:
18
+        mechanisms_classes.extend(behaviour_class.use)
19
+    return list(set(mechanisms_classes))  # Remove duplicates
20
+
21
+
22
+def initialize_subject(
23
+        simulation: 'Simulation',
24
+        subject: 'Subject',
25
+) -> None:
26
+    for mechanism_class in get_mechanisms_classes(subject):
27
+        subject.mechanisms[mechanism_class] = mechanism_class(
28
+            simulation=simulation,
29
+            subject=subject,
30
+        )
31
+
32
+    for behaviour_class in subject.behaviours_classes:
33
+        subject.behaviours[behaviour_class] = behaviour_class(
34
+            simulation=simulation,
35
+            subject=subject,
36
+        )

+ 110 - 0
synergine2/xyz.py View File

@@ -0,0 +1,110 @@
1
+from math import sqrt
2
+from math import degrees
3
+from math import acos
4
+
5
+from synergine2.simulation import Mechanism
6
+from synergine2.simulation import Simulation as BaseSimulation
7
+
8
+
9
+"""
10
+
11
+Positions are exprimed as tuple: (x, y, z) Considering start point at top left:
12
+
13
+    Z
14
+   #
15
+  #
16
+ #
17
+#-------------> X
18
+|.
19
+| .
20
+|  .
21
+|
22
+Y
23
+"""
24
+
25
+COLLECTION_XYZ = 'COLLECTION_XYZ'
26
+
27
+
28
+def get_distance_between_points(a: tuple, b: tuple) -> float:
29
+    return abs(sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2))
30
+
31
+
32
+def get_degree_from_north(a, b):
33
+    if a == b:
34
+        return 0
35
+
36
+    ax, ay = a[0], a[1]
37
+    bx, by = b[0], b[1]
38
+    Dx, Dy = ax, ay - 1
39
+    ab = sqrt((bx - ax) ** 2 + (by - ay) ** 2)
40
+    aD = sqrt((Dx - ax) ** 2 + (Dy - ay) ** 2)
41
+    Db = sqrt((bx - Dx) ** 2 + (by - Dy) ** 2)
42
+
43
+    degs = degrees(acos((ab ** 2 + aD ** 2 - Db ** 2) / (2 * ab * aD)))
44
+    if bx < ax:
45
+        return 360 - degs
46
+    return degs
47
+
48
+
49
+class XYZSubjectMixinMetaClass(type):
50
+    def __init__(cls, name, parents, attribs):
51
+        super().__init__(name, parents, attribs)
52
+        collections = getattr(cls, "collections", [])
53
+        if COLLECTION_XYZ not in collections:
54
+            collections.append(COLLECTION_XYZ)
55
+
56
+
57
+class XYZSubjectMixin(object, metaclass=XYZSubjectMixinMetaClass):
58
+    def __init__(self, *args, **kwargs):
59
+        """
60
+        :param position: tuple with (x, y, z)
61
+        """
62
+        self.position = kwargs.pop('position')
63
+        super().__init__(*args, **kwargs)
64
+
65
+
66
+class ProximityMechanism(Mechanism):
67
+    distance = 1
68
+    feel_collections = [COLLECTION_XYZ]
69
+    direction_round_decimals = 0
70
+    distance_round_decimals = 2
71
+
72
+    def run(self):
73
+        subjects = []
74
+        for feel_collection in self.feel_collections:
75
+            for subject in self.simulation.collections.get(feel_collection, []):
76
+                if subject == self.subject:
77
+                    continue
78
+
79
+                distance = round(
80
+                    self.get_distance_of(subject),
81
+                    self.distance_round_decimals,
82
+                )
83
+                if subject != self.subject and distance <= self.distance:
84
+                    direction = round(
85
+                        get_degree_from_north(
86
+                            self.subject.position,
87
+                            subject.position,
88
+                        ),
89
+                        self.direction_round_decimals,
90
+                    )
91
+                    subjects.append({
92
+                        'subject': subject,
93
+                        'direction': direction,
94
+                        'distance': distance,
95
+                    })
96
+
97
+        return subjects
98
+
99
+    def get_distance_of(self, subject: XYZSubjectMixin):
100
+        return get_distance_between_points(
101
+            self.subject.position,
102
+            subject.position,
103
+        )
104
+
105
+
106
+class Simulation(BaseSimulation):
107
+    """
108
+    TODO: xyz properties
109
+    """
110
+    pass

+ 88 - 0
synergine2/xyz_utils.py View File

@@ -0,0 +1,88 @@
1
+import collections
2
+
3
+
4
+def get_positions_from_str_representation(str_representation):
5
+    # TODO: Manage z axis (like ------------ as separator)
6
+    lines = str_representation.split("\n")  # One item per lines
7
+    lines = map(lambda l: l.strip().replace(' ', ''), lines)  # Remove spaces
8
+    lines = filter(lambda l: bool(l), lines)  # Only line with content
9
+    lines = list(lines)
10
+
11
+    width = len(lines[0])
12
+    height = len(lines)
13
+
14
+    if not width % 2 or not height % 2:
15
+        raise Exception(
16
+            'Width and height of your representation must be odd. '
17
+            'Actually it\'s {0}x{1}'.format(
18
+                width,
19
+                height,
20
+            ))
21
+
22
+    items_positions = collections.defaultdict(list)
23
+
24
+    start_x = - int(width / 2 - 0.5)
25
+    start_y = - int(height / 2 - 0.5)
26
+    start_z = 0
27
+
28
+    current_y = start_y
29
+    current_z = start_z
30
+
31
+    for line in lines:
32
+        current_x = start_x
33
+        for char in line:
34
+            items_positions[char].append((
35
+                current_x,
36
+                current_y,
37
+                current_z,
38
+            ))
39
+            current_x += 1
40
+        current_y += 1
41
+
42
+    return items_positions
43
+
44
+
45
+def get_str_representation_from_positions(
46
+    items_positions: dict,
47
+    separator='',
48
+    tabulation='',
49
+    start_with='',
50
+    end_with='',
51
+) -> str:
52
+    positions = []
53
+    for item_positions in items_positions.values():
54
+        positions.extend(item_positions)
55
+    positions = sorted(positions, key=lambda p: (p[2], p[1], p[0]))
56
+
57
+    str_representation = start_with + tabulation
58
+
59
+    start_x = positions[0][0]
60
+    start_y = positions[0][1]
61
+    start_z = positions[0][2]
62
+
63
+    current_y = start_y
64
+    current_z = start_z
65
+
66
+    for position in positions:
67
+        item = None
68
+        for parsed_item in items_positions:
69
+            if position in items_positions[parsed_item]:
70
+                item = parsed_item
71
+                break
72
+
73
+        if position[1] != current_y:
74
+            str_representation += "\n" + tabulation
75
+
76
+        if position[2] != current_z:
77
+            str_representation += '----' + "\n" + tabulation
78
+
79
+        added_value = item
80
+        if position[0] != start_x:
81
+            added_value = separator + added_value
82
+
83
+        str_representation += added_value
84
+        current_y = position[1]
85
+        current_z = position[2]
86
+
87
+    return str_representation + end_with
88
+

+ 8 - 0
tests/__init__.py View File

@@ -1,4 +1,12 @@
1 1
 
2
+# Used as parameters of str representations
3
+str_kwargs = dict(
4
+    separator=' ',
5
+    tabulation='            ',
6
+    start_with="\n",
7
+    end_with='\n        ',
8
+)
9
+
2 10
 
3 11
 class BaseTest(object):
4 12
     pass

+ 93 - 0
tests/test_life_game.py View File

@@ -0,0 +1,93 @@
1
+import collections
2
+from sandbox.life_game.simulation import Cell, Empty
3
+from synergine2.cycle import CycleManager
4
+from synergine2.simulation import Simulation, Subjects
5
+from synergine2.utils import initialize_subject
6
+from synergine2.xyz_utils import get_str_representation_from_positions
7
+from tests import BaseTest, str_kwargs
8
+
9
+
10
+class TestSimpleSimulation(BaseTest):
11
+    def test_cycles_evolution(self):
12
+        simulation = Simulation()
13
+        subjects = self._get_subjects(simulation)
14
+        simulation.subjects = subjects
15
+
16
+        cycle_manager = CycleManager(
17
+            subjects=subjects,
18
+        )
19
+
20
+        assert """
21
+            0 0 0 0 0
22
+            0 1 1 1 0
23
+            0 0 0 0 0
24
+        """ == self._get_str_representation_of_subjects(
25
+            subjects,
26
+        )
27
+
28
+        cycle_manager.next()
29
+
30
+        assert """
31
+            0 0 1 0 0
32
+            0 0 1 0 0
33
+            0 0 1 0 0
34
+        """ == self._get_str_representation_of_subjects(
35
+            subjects,
36
+        )
37
+
38
+        cycle_manager.next()
39
+
40
+        assert """
41
+            0 0 0 0 0
42
+            0 1 1 1 0
43
+            0 0 0 0 0
44
+        """ == self._get_str_representation_of_subjects(
45
+            subjects,
46
+        )
47
+
48
+    def _get_subjects(self, simulation: Simulation):
49
+        cells = Subjects(simulation=simulation)
50
+
51
+        for position in [
52
+            (-1, 0, 0),
53
+            (0, 0, 0),
54
+            (1, 0, 0),
55
+        ]:
56
+            cells.append(Cell(
57
+                simulation=simulation,
58
+                position=position,
59
+            ))
60
+
61
+        for position in [
62
+            (-2, -1, 0),
63
+            (-1, -1, 0),
64
+            (0, -1, 0),
65
+            (1, -1, 0),
66
+            (2, -1, 0),
67
+            (-2, 0, 0),
68
+            (2, 0, 0),
69
+            (-2, 1, 0),
70
+            (-1, 1, 0),
71
+            (0, 1, 0),
72
+            (1, 1, 0),
73
+            (2, 1, 0),
74
+        ]:
75
+            cells.append(Empty(
76
+                simulation=simulation,
77
+                position=position,
78
+            ))
79
+        return cells
80
+
81
+    def _get_str_representation_of_subjects(self, subjects: list):
82
+        items_positions = collections.defaultdict(list)
83
+
84
+        for subject in subjects:
85
+            if type(subject) == Cell:
86
+                items_positions['1'].append(subject.position)
87
+            if type(subject) == Empty:
88
+                items_positions['0'].append(subject.position)
89
+
90
+        return get_str_representation_from_positions(
91
+            items_positions,
92
+            **str_kwargs
93
+        )

+ 212 - 0
tests/test_xyz.py View File

@@ -0,0 +1,212 @@
1
+# -*- coding: utf-8 -*-
2
+from synergine2.simulation import Subject
3
+from synergine2.simulation import Subjects
4
+from synergine2.simulation import Simulation
5
+from synergine2.xyz import ProximityMechanism
6
+from synergine2.xyz import XYZSubjectMixin
7
+from synergine2.xyz_utils import get_positions_from_str_representation
8
+from synergine2.xyz_utils import get_str_representation_from_positions
9
+from tests import BaseTest
10
+from tests import str_kwargs
11
+
12
+
13
+class MySubject(XYZSubjectMixin, Subject):
14
+    pass
15
+
16
+
17
+class MyProximityMechanism(ProximityMechanism):
18
+    distance = 10
19
+
20
+
21
+class TestXYZ(BaseTest):
22
+    def test_proximity_mechanism_with_one(self):
23
+        simulation = Simulation()
24
+        subject = MySubject(simulation, position=(0, 0, 0))
25
+        other_subject = MySubject(simulation, position=(5, 0, 0))
26
+
27
+        simulation.subjects = Subjects(
28
+            [subject, other_subject],
29
+            simulation=simulation,
30
+        )
31
+
32
+        proximity_mechanism = MyProximityMechanism(
33
+            simulation=simulation,
34
+            subject=subject,
35
+        )
36
+
37
+        assert 5 == proximity_mechanism.get_distance_of(other_subject)
38
+        assert [{
39
+            'subject': other_subject,
40
+            'direction': 90.0,
41
+            'distance': 5.0,
42
+        }] == proximity_mechanism.run()
43
+
44
+    def test_proximity_mechanism_excluding(self):
45
+        simulation = Simulation()
46
+        subject = MySubject(simulation, position=(0, 0, 0))
47
+        other_subject = MySubject(simulation, position=(11, 0, 0))
48
+
49
+        simulation.subjects = Subjects(
50
+            [subject, other_subject],
51
+            simulation=simulation,
52
+        )
53
+
54
+        proximity_mechanism = MyProximityMechanism(
55
+            simulation=simulation,
56
+            subject=subject,
57
+        )
58
+
59
+        assert 11 == proximity_mechanism.get_distance_of(other_subject)
60
+        # other_subject is to far away
61
+        assert [] == proximity_mechanism.run()
62
+
63
+    def test_proximity_mechanism_with_multiple(self):
64
+        simulation = Simulation()
65
+        subject = MySubject(simulation, position=(0, 0, 0))
66
+        other_subjects = []
67
+
68
+        for i in range(3):
69
+            other_subjects.append(MySubject(simulation, position=(i, i, 0)))
70
+
71
+        simulation.subjects = Subjects([subject], simulation=simulation)
72
+        simulation.subjects.extend(other_subjects)
73
+
74
+        proximity_mechanism = MyProximityMechanism(
75
+            simulation=simulation,
76
+            subject=subject,
77
+        )
78
+
79
+        data = proximity_mechanism.run()
80
+        assert [
81
+            {
82
+                'direction': 0,
83
+                'subject': other_subjects[0],
84
+                'distance': 0.0,
85
+            },
86
+            {
87
+                'direction': 135.0,
88
+                'subject': other_subjects[1],
89
+                'distance': 1.41
90
+            },
91
+            {
92
+                'direction': 135.0,
93
+                'subject': other_subjects[2],
94
+                'distance': 2.83
95
+            },
96
+        ] == data
97
+
98
+    def test_str_representation_from_str(self):
99
+        str_ = """
100
+            0 0 1 0 0
101
+            0 1 1 1 0
102
+            0 0 1 0 0
103
+        """
104
+        items_positions = {
105
+            '0': [
106
+                (-2, -1, 0),
107
+                (-1, -1, 0),
108
+                (1, -1, 0),
109
+                (2, -1, 0),
110
+                (-2, 0, 0),
111
+                (2, 0, 0),
112
+                (-2, 1, 0),
113
+                (-1, 1, 0),
114
+                (1, 1, 0),
115
+                (2, 1, 0),
116
+            ],
117
+            '1': [
118
+                (0, -1, 0),
119
+                (-1, 0, 0),
120
+                (0, 0, 0),
121
+                (1, 0, 0),
122
+                (0, 1, 0),
123
+            ],
124
+        }
125
+        assert items_positions == get_positions_from_str_representation(str_)
126
+
127
+    def test_str_representation_to_str(self):
128
+        expected = """
129
+            0 0 1 0 0
130
+            0 1 1 1 0
131
+            0 0 1 0 0
132
+        """
133
+        items_positions = {
134
+            '0': [
135
+                (-2, -1, 0),
136
+                (-1, -1, 0),
137
+                (1, -1, 0),
138
+                (2, -1, 0),
139
+                (-2, 0, 0),
140
+                (2, 0, 0),
141
+                (-2, 1, 0),
142
+                (-1, 1, 0),
143
+                (1, 1, 0),
144
+                (2, 1, 0),
145
+            ],
146
+            '1': [
147
+                (0, -1, 0),
148
+                (-1, 0, 0),
149
+                (0, 0, 0),
150
+                (1, 0, 0),
151
+                (0, 1, 0),
152
+            ],
153
+        }
154
+
155
+        assert expected == \
156
+            get_str_representation_from_positions(
157
+                items_positions,
158
+                **str_kwargs
159
+            )
160
+
161
+    def test_str_representation_to_str_multi_levels(self):
162
+        expected = """
163
+            0 0 1 0 0
164
+            0 1 1 1 0
165
+            0 0 1 0 0
166
+            ----
167
+            0 0 0 0 0
168
+            0 0 1 0 0
169
+            0 0 0 0 0
170
+        """
171
+        items_positions = {
172
+            '0': [
173
+                (-2, -1, 0),
174
+                (-1, -1, 0),
175
+                (1, -1, 0),
176
+                (2, -1, 0),
177
+                (-2, 0, 0),
178
+                (2, 0, 0),
179
+                (-2, 1, 0),
180
+                (-1, 1, 0),
181
+                (1, 1, 0),
182
+                (2, 1, 0),
183
+                (-2, -1, 1),
184
+                (-1, -1, 1),
185
+                (1, -1, 1),
186
+                (2, -1, 1),
187
+                (-1, 0, 1),
188
+                (-2, 0, 1),
189
+                (2, 0, 1),
190
+                (-2, 1, 1),
191
+                (-1, 1, 1),
192
+                (1, 1, 1),
193
+                (2, 1, 1),
194
+                (2, -1, 1),
195
+                (1, 0, 1),
196
+                (2, 1, 1),
197
+            ],
198
+            '1': [
199
+                (0, -1, 0),
200
+                (-1, 0, 0),
201
+                (0, 0, 0),
202
+                (1, 0, 0),
203
+                (0, 1, 0),
204
+                (0, 0, 1),
205
+            ],
206
+        }
207
+
208
+        assert expected == \
209
+            get_str_representation_from_positions(
210
+                items_positions,
211
+                **str_kwargs
212
+            )