Skip to content

Instantly share code, notes, and snippets.

@imposeren
Last active September 30, 2020 00:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save imposeren/107bd5b311432ad40cdcfe959fb69c55 to your computer and use it in GitHub Desktop.
Save imposeren/107bd5b311432ad40cdcfe959fb69c55 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""Dummy straight-forward implementation of Langton's Ant.
More ugly version but with better defaults for stdout drawings:
https://gist.github.com/imposeren/f8dcfa1791b6efb296a2b9428df15dfa
"""
import argparse
import math # Only used for "example code" that is not needed really.
import os
import shutil
import sys
import time
import unittest
from collections import namedtuple
from copy import deepcopy
from enum import Enum, auto
from itertools import product
# Note both for "Position" vectors and for state matrix it may be easier to use
# `np.array` (more convenient indexing and vector+vector support out of the
# box), but for "demonstration purposes" following code will not use numpy
class Vector(namedtuple('Vector', ['x', 'y'])):
__slots__ = ()
def __add__(self, other):
cls = type(self)
if not isinstance(other, cls):
other = cls(other[0], other[1])
return cls(self.x + other.x, self.y + other.y)
class Direction(int, Enum):
"""Four directions where ant can be headed.
Incrementing direction by 1 means clockwise rotation by 90 degrees.
"""
North = 0
East = 1
South = 2
West = 3
# Aliases:
N = North
Up = North
U = North
E = East
Right = East
R = East
S = South
Down = South
D = South
W = West
Left = West
L = West
@classmethod
def _missing_(cls, value):
if value > 3 or value < 0:
value = value % 4
return cls(value)
@property
def shift_vector(self):
cls = type(self)
if self == cls.North:
v = (0, -1)
elif self == cls.South:
v = (0, 1)
elif self == cls.West:
v = (-1, 0)
elif self == cls.East:
v = (1, 0)
else:
raise NotImplementedError(
'Direction subclass defines member with unknown shift vector'
)
# Universal approach may look like this:
# Use West as zero value and go counter-closkwise:
pre_rad_value = (1-self.value) % 4
# Convert value to radians:
rad_value = pre_rad_value / 2 * math.pi
x = math.cos(rad.value)
y = -math.sin(rad.value)
# We should only shift to integer coords of specific cell,
# so shift_vector must be rounded
v = round(x), round(y)
return Vector(*v)
def __add__(self, other):
cls = type(self)
if isinstance(other, cls):
raise TypeError(
'Can\'t add directions together, because they are not "angles"'
)
# They can be angles if one of them is considered as a start of
# coordinates but that will complicate things (should North be
# considered as zero or east as in math? Should they be
# incrementing clockwise or counter-clockwise?)
return cls(self.value+other)
class EdgeHandling(Enum):
OPPOSITES_CONNECTED = auto()
#: Going through edge means starging near opposite edge
INFINITE = auto()
#: Game field is not limited (automatically grows)
EXTRA_ROTATION = auto()
#: If rotation will cause ant to hit a wall, then he should rotate more.
class LangtonsAnt:
"""Initialize a handler for Langtons's Ant "game"."""
def __init__(
self,
state_or_size,
start_position=(0, 0),
start_direction=Direction.North,
default_filler=False,
edge_handling=EdgeHandling.OPPOSITES_CONNECTED,
grow_step=5):
try:
# Initialized with size
width, height = tuple(map(int, state_or_size))
self._cells_state = [
[default_filler for _i in range(width)]
for _j in range(height)
]
except TypeError:
self._cells_state = state_or_size
width = len(state_or_size[0])
height = len(state_or_size)
self._width = width
self._height = height
self._default_filler = default_filler
self._edge_handling = edge_handling
self._grow_step = grow_step
if isinstance(start_position, Vector):
self._ant_position = start_position
else:
self._ant_position = Vector(*start_position)
self._ant_direction = start_direction
self._validate_size()
self._reset_prev_states()
self._prev_cells_state = None
self._prev_ant_state = None
def _validate_size(self):
if self.width < 2 or self.height < 2:
if self.edge_handling == EdgeHandling.EXTRA_ROTATION:
raise ValueError(
'Size of limited field should be at least 2x2.'
)
elif self.edge_handling == EdgeHandling.OPPOSITES_CONNECTED:
if width >= 4 or height >= 4:
# Allow case when field is a single line
pass
else:
raise ValueError(
'Size of field with connected edges should be at '
'least 1x4 or 4x1.'
)
elif self.edge_handling == EdgeHandling.INFINITE:
# Field may grow, so it's ok
pass
else:
raise NotImplementedError(
'Handling of field smaller than 2x2 is not implemented for '
f'{edge_handling} behaviour.'
)
def _reset_prev_states(self):
self._prev_cells_state = None
self._prev_ant_state = None
@property
def width(self):
return self._width
@property
def height(self):
return self._height
@property
def edge_handling(self):
return self._edge_handling
@property
def state(self):
"""State of cells in game."""
return deepcopy(self._cells_state)
@property
def full_state(self):
"""Return list of tuples for each cell containing `(cell_state, ant_info)`.
In the tuple `ant_info` is either `None` or member of `:py:cls:Direction`
reperesenting direction of ant located in the cell.
"""
full_state = [
[(cell_state, None) for cell_state in row]
for row in self._cells_state
]
x, y = self._ant_position
full_state[y][x] = (full_state[y][x][0], self._ant_direction)
return full_state
@property
def ant_position(self):
return self._ant_position
@property
def ant_direction(self):
return self._ant_direction
@property
def ant_state(self):
return (self.ant_position, self.ant_direction)
def draw(self, mapping=None, border=' '):
if len(border) >= 2:
raise ValueError('Only single character borders are supported')
if mapping is None:
mapping = {
(False, None): ' ',
(False, Direction.Up): '⇧',
(False, Direction.Down): '⇩',
(False, Direction.Left): '⇦',
(False, Direction.Right): '⇨',
(True, None): '░',
(True, Direction.Up): '▀',
(True, Direction.Down): '▃',
(True, Direction.Left): '▌',
(True, Direction.Right): '▐',
}
if border:
print(border*(self.width+2))
for y, row in enumerate(self.state):
print(border, end='')
for x, cell_state in enumerate(row):
key = (
cell_state,
self.ant_direction if self.ant_position == (x, y) else None,
)
print(mapping[key], end='')
print(border)
if border:
print(border*(self.width+2))
def next(self):
self._prev_cells_state = self.state
self._prev_ant_state = self.ant_state
x, y = self._ant_position
current_cell = self._cells_state[y][x]
# Rotate the ant:
rotation = 1 - current_cell*2
# Note: ^^^ this can be done with `if c_cell: d+=1; else: d-=1`, but doing
# this "mathematically" saves a little bit of time.
self._ant_direction += rotation
# Flip current cell:
self._cells_state[y][x] = not current_cell
# Move the ant:
new_position = None
while new_position is None:
new_position = self._ant_position + self._ant_direction.shift_vector
out_of_bounds = (
new_position.x+1 > self.width
or
new_position.y+1 > self.height
or
new_position.x < 0
or
new_position.y < 0
)
if out_of_bounds:
if self.edge_handling == EdgeHandling.OPPOSITES_CONNECTED:
new_position = self._move_through_connected_edge(new_position)
elif self.edge_handling == EdgeHandling.EXTRA_ROTATION:
self._ant_direction += rotation
new_position = None # Recalculate new position
elif self.edge_handling == EdgeHandling.INFINITE:
new_position = self._grow_field(new_position)
self._ant_position = new_position
def revert(self):
if not (self._prev_cells_state and self._prev_ant_state):
raise RuntimeError("Previous state unknown!")
self._cells_state = self._prev_cells_state
self._ant_position, self._ant_direction = self._prev_ant_state
self._reset_prev_states()
def force_cell(self, value, x=None, y=None):
"""Force state of the cell at some coords (defaults to ant position).
"""
if (x, y) == (None, None):
x, y = self.ant_position
else:
if self.edge_handling == EdgeHandling.OPPOSITES_CONNECTED:
x, y = self._move_through_connected_edge((x, y))
elif self.edge_handling == EdgeHandling.INFINITE:
x, y = self._grow_field(new_position)
else:
# Just let IndexError to be raised if x or y are incorrect.
pass
self._cells_state[y][x] = value
self._reset_prev_states()
def force_ant_direction(self, new_direction):
self._ant_direction = new_direction
self._reset_prev_states()
def _move_through_connected_edge(self, position):
return_vector = True
if not isinstance(position, Vector):
return_vector = False
position = Vector(*position)
# If ant moves through the edge, then move him to opposite edge:
normalized_x = position.x % self.width
normalized_y = position.y % self.height
if return_vector:
return Vector(normalized_x, normalized_y)
else:
return (normalized_x, normalized_y)
def _grow_field(self, new_position):
if not isinstance(new_position, Vector):
new_position = Vector(*new_position)
prepend_rows = 0
append_rows = 0
prepend_cols = 0
append_cols = 0
height_change = 0
width_change = 0
if new_position.y < 0:
prepend_rows = abs(new_position.y)
prepend_rows = max(prepend_rows, self._grow_step)
height_change += prepend_rows
elif new_position.y >= self.height:
append_rows = 1 + new_position.y - self.height
append_rows = max(append_rows, self._grow_step)
height_change += append_rows
if new_position.x < 0:
prepend_cols = abs(new_position.x)
prepend_cols = max(prepend_cols, self._grow_step)
width_change += prepend_cols
elif new_position.x >= self.width:
append_cols = 1 + new_position.x - self.width
append_cols = max(append_cols, self._grow_step)
width_change += append_cols
new_height = self.height + height_change
new_width = self.width + width_change
if prepend_rows:
# Note: if dequeue is used with extendleft then it's better than
# current approach. And extend_left is also better because it does
# not create a new object (changes existing one)
self._cells_state = [
[self._default_filler for _i in range(new_width)]
for _j in range(prepend_rows)
] + self._cells_state
new_position = new_position + (0, prepend_rows)
if append_rows:
self._cells_state.extend([
[self._default_filler for _i in range(new_width)]
for _j in range(append_rows)
])
if prepend_cols or append_cols:
for y, current_row in enumerate(self._cells_state[:]):
# Note: See note about prepend_rows.
self._cells_state[y] = (
[self._default_filler for _i in range(prepend_cols)]
+
current_row
+
[self._default_filler for _i in range(append_cols)]
)
if prepend_cols:
new_position = new_position + (prepend_cols, 0)
self._height = new_height
self._width = new_width
return new_position
class TestDirection(unittest.TestCase):
def test_aliases(self):
self.assertEqual(Direction.North, Direction.N)
self.assertEqual(Direction.North, Direction.Up)
self.assertEqual(Direction.North, Direction.U)
self.assertEqual(Direction.East, Direction.E)
self.assertEqual(Direction.East, Direction.Right)
self.assertEqual(Direction.East, Direction.R)
self.assertEqual(Direction.South, Direction.S)
self.assertEqual(Direction.South, Direction.Down)
self.assertEqual(Direction.South, Direction.D)
self.assertEqual(Direction.West, Direction.W)
self.assertEqual(Direction.West, Direction.Left)
self.assertEqual(Direction.West, Direction.L)
def test_rotation(self):
self.assertEqual(Direction(Direction.W+1), Direction.N)
self.assertEqual(Direction(Direction.N-1), Direction.W)
self.assertEqual(Direction(Direction.W+3), Direction.S)
self.assertEqual(Direction(Direction.E+1), Direction.S)
self.assertEqual(Direction(Direction.E+2), Direction.W)
def test_shift_vectors(self):
self.assertEqual(Direction.Up.shift_vector, (0, -1))
self.assertEqual(Direction.Down.shift_vector, (0, 1))
self.assertEqual(Direction.Left.shift_vector, (-1, 0))
self.assertEqual(Direction.Right.shift_vector, (1, 0))
class TestLangtonsAntSimple(unittest.TestCase):
"""Test simple moves of LangtonsAnt game."""
def setUp(self):
super().setUp()
self.game = LangtonsAnt(
[
[False for _i in range(3)]
for _j in range(3)
],
(1, 1),
Direction.North,
)
def test_force_cell(self):
game = self.game
self.assertEqual(game.state[1][1], False)
game.force_cell(True)
self.assertEqual(game.state[1][1], True)
game.force_cell(False)
self.assertEqual(game.state[1][1], False)
game.force_cell(True, x=1, y=2)
self.assertEqual(game.state[2][1], True)
game.force_cell(False, x=1, y=2)
self.assertEqual(game.state[2][1], False)
def test_force_ant_direction(self):
D = Direction
game = self.game
self.assertEqual(game.ant_state, ((1, 1), D.North))
self.assertEqual(game.ant_direction, D.North)
for new_d in (D.East, D.South, D.West, D.North):
game.force_ant_direction(new_d)
self.assertEqual(game.ant_direction, new_d)
def test_revert(self):
game = self.game
original_state = deepcopy(game.state)
self.assertEqual(game.state, original_state)
game.next()
self.assertNotEqual(game.state, original_state)
game.revert()
self.assertEqual(game.state, original_state)
def test_moves(self):
"""Simple move tests.
Ant should determine rotation direction, rotate,
flip current cell and move.
"""
D = Direction
game = self.game
cell_states = [0, 1]
starting_directions = [D.U, D.R, D.D, D.L]
for cell_state, direction in product(cell_states, starting_directions):
with self.subTest(cell_state=cell_state, direction=direction):
game.force_cell(cell_state)
game.force_ant_direction(direction)
# Flip color:
new_state = (not cell_state)
# Rotate:
if not cell_state:
new_direction = D(direction + 1)
else:
new_direction = D(direction - 1)
# Move:
new_x, new_y = game.ant_position + new_direction.shift_vector
game.next()
self.assertEqual(
game.full_state[1][1],
(not cell_state, None),
"Initial cell should be flipped and without the Ant",
)
self.assertEqual(
game.full_state[new_y][new_x],
(False, new_direction),
"Ant should be moved to new cell without changing it's state"
)
game.revert()
class TestLangtonsAntLimitedFieldProcess(unittest.TestCase):
"""Test different game states progression.
Legend for symbols used on game state "doc-representation":
* `⇧⇩⇦⇨` — ant standing on False cell heading in some Direction.
* `↑↓←→` — ant standing on `True` cell heading in some Direction.
* `0` — empty `False` cell.
* `1` — empty `True` cell.
* `#` — empty cell with unknown color that is not important for current
context.
* `TV<>` -- ant headed in some Direction and MOVING there at the time.
"""
def setUp(self):
super().setUp()
self.game2x2 = LangtonsAnt(
[
[False, False],
[False, True],
],
(0, 0),
Direction.North,
)
self.game11x11 = LangtonsAnt(
(11, 11),
(5, 5),
Direction.W,
)
def test_small_game(self):
"""Test ant on small cyclic field."""
# Description of state changes:
# "Cyclic" field means that right edge of field is connected to the left
# edge, and top edge to the bottom edge. Examples:
# 1> transitions to →#
# ## ##
#
# #0 transitions to #⇩
# #V ##
# Initial state (0):
# ⇧0
# 01
# Before flipping color:
# ⇨0
# 01
# Next state (1):
# 1⇨
# 01
# Next state (2):
# 11
# 0↓
# Befor flipping color:
# 11
# 0→
# Next state (3):
# 11
# ⇨0
# Next state (4):
# ↓1
# 10
# Next state (5):
# 0→
# 10
# Next state (6):
# 00
# 1⇧
# Next state (7):
# 00
# →1
# Next state (8):
# ⇧0
# 01
# (Equals to state 0)
game = self.game2x2
self.assertEqual(
game.full_state,
[
[(0, Direction.U), (0, None)],
[(0, None), (1, None)],
]
)
game.next()
self.assertEqual(
game.full_state,
[
[(1, None), (0, Direction.R)],
[(0, None), (1, None)],
]
)
game.next()
self.assertEqual(
game.full_state,
[
[(1, None), (1, None)],
[(0, None), (1, Direction.D)],
]
)
game.next()
self.assertEqual(
game.full_state,
[
[(1, None), (1, None)],
[(0, Direction.R), (0, None)],
]
)
game.next()
self.assertEqual(
game.full_state,
[
[(1, Direction.D), (1, None)],
[(1, None), (0, None)],
]
)
game.next()
self.assertEqual(
game.full_state,
[
[(0, None), (1, Direction.R)],
[(1, None), (0, None)],
]
)
game.next()
self.assertEqual(
game.full_state,
[
[(0, None), (0, None)],
[(1, None), (0, Direction.U)],
]
)
game.next()
self.assertEqual(
game.full_state,
[
[(0, None), (0, None)],
[(1, Direction.R), (1, None)],
]
)
game.next()
self.assertEqual(
game.full_state,
[
[(0, Direction.U), (0, None)],
[(0, None), (1, None)],
]
)
def test_big_game(self):
"""Test bigger game (using animation from wiki as reference)."""
game = self.game11x11
game.next()
# Filled and moved up:
self.assertEqual(game.full_state[5][5], (1, None))
self.assertEqual(game.full_state[4][5], (0, Direction.U))
game.next()
# Filled and moved right:
self.assertEqual(game.full_state[4][5], (1, None))
self.assertEqual(game.full_state[4][6], (0, Direction.R))
game.next()
# Filled and moved down:
self.assertEqual(game.full_state[4][6], (1, None))
self.assertEqual(game.full_state[5][6], (0, Direction.D))
game.next()
# Filled and moved left (to already filled cell):
self.assertEqual(game.full_state[5][6], (1, None))
self.assertEqual(game.full_state[5][5], (1, Direction.L))
game.next()
# Cleared and moved down:
self.assertEqual(game.full_state[5][5], (0, None))
self.assertEqual(game.full_state[6][5], (0, Direction.D))
game.next()
# Filled and moved left:
self.assertEqual(game.full_state[6][5], (1, None))
self.assertEqual(game.full_state[6][4], (0, Direction.L))
def test_big_game_rotation_behaviour(self):
"""Test bigger game using rotation behaviour for edges."""
game = LangtonsAnt(
(3, 3),
(1, 1),
Direction.W,
edge_handling=EdgeHandling.EXTRA_ROTATION,
)
for _ in range(6):
game.next()
expected_state = [
[0, 1, 1],
[0, 0, 1],
[0, 1, 0],
]
self.assertEqual(
game.state,
expected_state,
)
for _ in range(4):
game.next()
expected_state = [
[0, 1, 1],
[1, 1, 1],
[1, 0, 0],
]
self.assertEqual(
game.state,
expected_state,
)
# Next move from x=2, y=2 should go down to (x=2, y=3)), but it can't,
# so it will go to left (x=1, y=2).
# But this does not affect expected state of cells yet (only state of
# ant).
game.next()
expected_state = [
[0, 1, 1],
[1, 1, 1],
[1, 0, 1],
]
self.assertEqual(
game.state,
expected_state,
)
self.assertEqual(
game.ant_position,
(1, 2),
"Ant should not move beyond board."
)
# Next move should be different from infinite game field.
game.next()
expected_state = [
[0, 1, 1],
[1, 1, 1],
[1, 1, 1],
]
self.assertEqual(
game.state,
expected_state,
)
self.assertEqual(
game.ant_position,
(1, 1),
)
class TestLangtonsAntInfiniteFieldProcess(unittest.TestCase):
def test_big_game(self):
"""Test similar to TestLangtonsAntLimitedFieldProcess.test_big_game but on infinite board."""
grow_step = 3
initial_widht = 2
initial_height = 2
game = LangtonsAnt(
(initial_widht, initial_height),
(0, 0),
Direction.W,
edge_handling=EdgeHandling.INFINITE,
grow_step=grow_step,
)
for _ in range(6):
game.next()
new_width = initial_widht + grow_step
new_height = initial_height + grow_step
expected_state = [
[0 for _ in range(new_width)]
for _j in range(new_height)
]
expected_state[-3][-2:] = [1, 1]
expected_state[-2][-2:] = [0, 1]
expected_state[-1][-2:] = [1, 0]
self.assertEqual(
game.state,
expected_state,
)
game.next()
expected_state[-1][-3:] = [1, 1, 0]
self.assertEqual(
game.state,
expected_state,
)
for _ in range(6):
game.next()
expected_state.extend([
[0 for _i in range(new_width)]
for _j in range(grow_step)
])
expected_state[-6][-3:] = [0, 1, 1]
expected_state[-5][-3:] = [1, 1, 1]
expected_state[-4][-3:] = [1, 0, 1]
expected_state[-3][-3:] = [0, 1, 1]
self.assertEqual(
game.state,
expected_state,
)
# Do all the remaining moves from wiki animation:
# https://upload.wikimedia.org/wikipedia/commons/0/09/LangtonsAntAnimated.gif
for _ in range(200-6-6-1-1):
game.next()
expected_image = (
" ",
" ",
" ▓▓ ",
" ▓▓▓▓▓▓ ",
" ▓ ▓ ▓ ▓",
" ▓ ▓ ▓ ▓",
" ▓ ▓▓ ▓ ▓",
" ▓ ▓ ▓ ▓",
" ▓ ▓▓▓ ▓▓ ",
" ▓▓▓▓▓ ▓ ",
" ▓▓ ",
)
expected_state = [
[True if char == '▓' else False for char in image_row]
for image_row in expected_image
]
self.assertEqual(
game.state,
expected_state,
)
self.assertEqual(
game.ant_position,
(9, 10),
)
def __parse_start_coord(raw_coord_value, full_size, fallback=0):
if raw_coord_value:
if raw_coord_value.endswith('%'):
numeric_str = raw_coord_value[:-1]
val_is_percents = True
else:
numeric_str = raw_coord_value
val_is_percents = False
coord_value = int(numeric_str)
if val_is_percents:
coord_value = int(full_size*coord_value/100)
return coord_value
else:
return fallback
def main():
parser = argparse.ArgumentParser(
description="Draw Langton's ant state or run tests"
)
parser.add_argument(
'--test', dest='run_tests', action='store_const', const=True,
default=None,
help=(
'Run the tests (default: False if drawing to terminal, true '
'otherwise'
)
)
parser.add_argument(
'--width', dest='draw_width', type=int, default=None,
metavar='NUM_COLS',
help='Drawing width in columns (defaults to terminal width minus 3)'
)
parser.add_argument(
'--height', dest='draw_height', type=int, default=None,
metavar='NUM_LINES',
help='Drawing width in lines (defaults to terminal height minus 3)'
)
parser.add_argument(
'--frame-duration', dest='frame_duration', type=float, default=0.1,
help=(
'Number of seconds for displaying one iteration (default: '
'%(default)s)'
),
)
parser.add_argument(
'num_iterations', type=int, default=0, nargs='?',
metavar='N',
help=(
'Number of moves ant should make. Required to output drawing in '
'terminal'
),
)
parser.add_argument(
'start_x', default=None, nargs='?',
metavar='AntX',
help=(
'Starting X coord of ant in percents of width if ends with %% or as plain '
'index otherwise (default: around 50%%)'
)
)
parser.add_argument(
'start_y', default=None, nargs='?',
metavar='AntY',
help=(
'Starting Y coord of ant in percents of height if ends with %% or as plain '
'index otherwise (default: around 50%%)'
)
)
parser.add_argument(
'--start-direction', dest='start_direction', default='Left',
choices=['Up', 'Down', 'Left', 'Right'],
metavar='ARROW_DIRECTION',
help=(
'Starting direction of the ant '
'(default: %(default)s). '
'Choices: %(choices)s'
),
)
parser.add_argument(
'--edge-handling', dest='edge_handling', default='OPPOSITES_CONNECTED',
choices=['OPPOSITES_CONNECTED', 'EXTRA_ROTATION'],
metavar='BEHAVIOUR',
help=(
'How ant should behave when it\'s going to hit edge of drawing '
'(default: %(default)s). '
'Choices: %(choices)s'
)
)
args = vars(parser.parse_args(sys.argv[1:]))
run_tests = args['run_tests']
num_iterations = args['num_iterations']
frame_duration = args['frame_duration']
print_drawing = num_iterations >= 1
if run_tests is None:
run_tests = (args['num_iterations'] == 0)
if run_tests:
unittest.main()
if print_drawing:
draw_width = args['draw_width']
draw_height = args['draw_height']
if not (draw_width and draw_height):
term_size = shutil.get_terminal_size((1, 1))
term_w, term_h = term_size.columns, term_size.lines
if draw_width is None:
draw_width = max(term_w - 3, 0)
if draw_height is None:
draw_height = max(term_h - 3, 0)
if draw_width < 4 or draw_height < 4:
print(
'Drawing size too small or info about terminal size is not '
'available.',
file=sys.stderr,
)
return sys.exit(1)
if args['start_direction'] == 'Left':
start_x_fallback_idx = 25
start_y_fallback_idx = 30
elif args['start_direction'] == 'Up':
start_x_fallback_idx = max(0, draw_width - 30)
start_y_fallback_idx = 25
elif args['start_direction'] == 'Right':
start_x_fallback_idx = max(0, draw_width - 25)
start_y_fallback_idx = max(0, draw_height - 30)
elif args['start_direction'] == 'Down':
start_x_fallback_idx = 30
start_y_fallback_idx = max(0, draw_height - 25)
try:
start_x = __parse_start_coord(
args['start_x'],
draw_width,
fallback=min(
int(draw_width*0.5),
start_x_fallback_idx,
),
)
except ValueError:
print(
f'Can\'t parse `AntX` value'
)
return sys.exit(1)
try:
start_y = __parse_start_coord(
args['start_y'],
draw_height,
fallback=min(
int(draw_height*0.5),
start_y_fallback_idx
),
)
except ValueError:
print(
f'Can\'t parse `AntY` value'
)
return sys.exit(1)
game = LangtonsAnt(
(draw_width, draw_height),
(start_x, start_y),
start_direction=getattr(Direction, args['start_direction']),
edge_handling=getattr(EdgeHandling, args['edge_handling']),
)
try:
# Hide cursor:
if os.name == 'posix':
print('\033[?25l', end='', flush=True)
# Do the drawing:
if frame_duration <= 0:
print('Calculating game state... 0%')
frame_duration = 3
skip_frames = True
else:
skip_frames = False
last_draw_time = time.time()
cleared = False
for i in range(num_iterations-1):
needs_draw = False
# Update game state:
game.next()
# Ensure frame duration for single drawing:
t_now = time.time()
next_draw_time = last_draw_time + frame_duration
remaining_time = next_draw_time - t_now
if remaining_time <= 0:
last_draw_time = t_now
needs_draw = True
elif not skip_frames:
time.sleep(remaining_time)
last_draw_time = next_draw_time
needs_draw = True
if needs_draw:
if not cleared:
# Clear screen:
os.system('cls' if os.name == 'nt' else 'clear')
cleared = True
else:
# Move cursor to start of the term:
if os.name == 'posix':
print('\033[0;0H', end='')
elif os.name == 'nt':
os.system('cls')
progress = int(100*i/num_iterations)
game.draw()
print(f'Calculating game state... {progress}%')
finally:
# Move and show cursor:
if os.name == 'posix':
print(f'\033[{draw_height+3};0H', end='', flush=True)
print('\033[?25h', end='')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment