6
\$\begingroup\$

I'd found myself getting back into programming, and had recalled a fascinating little 'game' many might know of. I'm a very inexperienced programmer, and I know this is sloppy, but I'd wanted to make something that had a sort of pseudo-cellular life kind of behavior.

There's no real pattern to their movement, but it does create a quite mesmerizing effect. All it does is generate a 'turtle' that wanders around turning tiles to the 'on' state until it dies after about 10 steps. If it encounters an 'on' tile, the turtle creates a new turtle in an adjacent space.

I'm well aware I have this really horribly optimized, but I'm hoping to expand upon it and probably just rewrite it more sanely. I used a little package called 'tdl' for ASCII graphics because I enjoy them and I really don't understand matplotlab animation at all or animation in general.

Controls:

  • Press 2 or 3 (not numpad) to add a new turtle value at a random position
  • Press Enter to increment one 'step'
  • Press c to clear the board
  • Press p to print the current population to the console

This is my bag of colors. It'll ask if you want to download upon opening the link.

This is the font used:

enter image description here

from random import randint #Woot! My turtles live life with no aim! Just livin on psudo-random whims XD

import numpy as np #You all know what this is, but I barely do

import tdl #A neat little package for handling ascii graphics. Just using it because it's very simple and fun.... and I don't really understand matplotlib animations....

import colors #Just a big bag of color values

SCREEN_WIDTH = 80
SCREEN_HEIGHT = 80

FONTSET = 'arial10x10.png'

LIMIT_FPS = 20

MAP_WIDTH = 100
MAP_HEIGHT = 80

BOARD = np.zeros( (MAP_WIDTH, MAP_HEIGHT) )



class TurtleBrain():
    def __init__(self, pattern):
        self.flag = True
        self.pattern = pattern


    def process(self):
        """Handles the 'steps' for each turtle value
        I did it this way so that I could later decide to add new values with different movements and behaviors
        or have several different types of life on the board at once, if I so choose to impliment them
        """
        while self.pattern == 'original':
            if self.flag:
                self.move_twos()
                self.flag = (not self.flag)
                break
            else:
                self.move_threes()
                self.flag = (not self.flag)
                break



    def move_twos(self):
        """
        I'm sure there are so many better ways to do this:
        Form lists of coordinates and data
        Make a dictionary of coordinate keys with data values?
        Sparse arrays?
        """
        #Find every 2 turtle
        for y in range(MAP_HEIGHT-1):
            for x in range(MAP_WIDTH-1):
                cell = BOARD[x, y]
                if 2.0 <= cell <= 2.2:
                    failed = True
                    while True:
                        dx = randint(-1, 1)
                        dy = randint(-1, 1)
                        new_x = x + dx
                        new_y = y + dy
                        if dx == 0 and dy == 0:
                            continue
                        else:
                            failed = False
                            break

                    new_cell = BOARD[int(new_x), int(new_y)]

                    if new_cell == 1.0:

                        BOARD[int(new_x), int(new_y)] = BOARD[int(x), int(y)] + 1

                        BOARD[int(x), int(y)] = 0.0

                        self.make_child(new_x, new_y)



                    elif new_cell == 0.0:

                        BOARD[int(new_x), int(new_y)] = BOARD[int(x), int(y)] + 1

                        BOARD[int(x), int(y)] = 1.0

                    else:
                        BOARD[int(new_x), int(new_y)] += .01





    def move_threes(self):
        #find every 3 turtle
        for y in range(MAP_HEIGHT-1):
            for x in range(MAP_WIDTH-1):
                cell = BOARD[x, y]
                if 3.0 <= cell <= 3.2:

                    failed = True
                    while True:
                        dx = randint(-1, 1)
                        dy = randint(-1, 1)
                        new_x = x + dx
                        new_y = y + dy
                        if dx == 0 and dy == 0:
                            continue
                        else:
                            failed = False
                            break

                    new_cell = BOARD[int(new_x), int(new_y)]

                    if new_cell == 1.0:
                        BOARD[int(new_x), int(new_y)] = BOARD[int(x), int(y)] - 1

                        BOARD[int(x), int(y)] = 0.0


                        self.make_child(new_x, new_y)

                    elif new_cell == 0.0:
                        BOARD[int(new_x), int(new_y)] = BOARD[int(x), int(y)] - 1

                        BOARD[int(x), int(y)] = 1.0

                    else:
                        BOARD[int(new_x), int(new_y)] += .01



    def move_fours(self):
        """Planned as a predator type to cull the 2 and 3 hoards mwuahaha"""
        for x in BOARD:
            for y in BOARD[x]:
                cell = BOARD[x, y]
                if 4.0 <= cell <= 4.2:
                    pass





    def make_child(self, px, py):
        failed = True
        while True:
            child_x = px + randint(-1, 1)
            child_y = py + randint(-1, 1)
            if BOARD[int(child_x), int(child_y)] == 0.0 or BOARD[int(child_x), int(child_y)] == 1.0:
                failed = False
                break

        if not failed:
            BOARD[int(child_x), int(child_y)] = 2.0
            #print("I made a child at (%d, %d)" % (child_x, child_y))

class TimeStep():
  def __init__(self, age_value):
      self.age_value = age_value

  def process(self):

    for y in range(MAP_HEIGHT):
        for x in range(MAP_WIDTH):
            if 2.0 <= BOARD[x, y] <= 3.1:
                #print("Test?")
                BOARD[x, y] += self.age_value
                #print(str(BOARD[x, y]) + " at (%d, %d)" % (x, y))
            if  2.1 <= BOARD[x, y] <= 2.9 or 3.1 <= BOARD[x, y] <= 3.9:
                BOARD[x, y] = 0.0
                #print("Turtle at (%d, %d) died" % (x, y))
                pass


class RenderProcessor():
    def __init__(self, _con, _root):
        super().__init__()
        self._con = _con
        self._root = _root

    def process(self):
        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):


                if BOARD[x, y] == 0.0:
                    self._con.draw_char(x, y, '#', fg=colors.dark_grey, bg=colors.darker_grey)
                #elif  2.0 <= BOARD[x, y] <= 2.99 or 3.0 <= BOARD[x, y] <= 3.99:
                #    self._con.draw_char(x, y, '@', fg=colors.dark_green, bg=colors.black)
                elif  2.0 <= BOARD[x, y] <= 2.01 or 3.0 <= BOARD[x, y] <= 3.01:
                    self._con.draw_char(x, y, '0', fg=colors.dark_green, bg=colors.black)
                elif 2.01 <= BOARD[x, y] <= 2.02 or 3.01 <= BOARD[x, y] <= 3.02:
                    self._con.draw_char(x, y, '1', fg=colors.dark_lime, bg=colors.black)
                elif 2.02 <= BOARD[x, y] <= 2.03 or 3.02 <= BOARD[x, y] <= 3.03:
                    self._con.draw_char(x, y, '2', fg=colors.dark_red, bg=colors.black)
                elif 2.03 <= BOARD[x, y] <= 2.04 or 3.03 <= BOARD[x, y] <= 3.04:
                    self._con.draw_char(x, y, '3', fg=colors.dark_blue, bg=colors.black)
                elif 2.04 <= BOARD[x, y] <= 2.05 or 3.04 <= BOARD[x, y] <= 3.05:
                    self._con.draw_char(x, y, '4', fg=colors.dark_purple, bg=colors.black)
                elif 2.05 <= BOARD[x, y] <= 2.06 or 3.05 <= BOARD[x, y] <= 3.06:
                    self._con.draw_char(x, y, '5', fg=colors.dark_violet, bg=colors.black)
                elif 2.06 <= BOARD[x, y] <= 2.07 or 3.06 <= BOARD[x, y] <= 3.07:
                    self._con.draw_char(x, y, '6', fg=colors.dark_sky, bg=colors.black)
                elif 2.07 <= BOARD[x, y] <= 2.08 or 3.07 <= BOARD[x, y] <= 3.08:
                    self._con.draw_char(x, y, '7', fg=colors.dark_cyan, bg=colors.black)
                elif 2.08 <= BOARD[x, y] <= 2.09 or 3.08 <= BOARD[x, y] <= 3.09:
                    self._con.draw_char(x, y, '8', fg=colors.dark_cyan, bg=colors.black)
                elif 2.09 <= BOARD[x, y] < 2.1 or 3.09 <= BOARD[x, y] <= 3.1:
                    self._con.draw_char(x, y, '9', fg=colors.darkest_pink, bg=colors.black)
                else:
                    self._con.draw_char(x, y, '#', fg=colors.darker_green, bg=colors.dark_green)


        self._root.blit(self._con, 0, 0, MAP_WIDTH, MAP_HEIGHT, 0, 0)
        tdl.flush()


##################################################################
tdl.set_font(FONTSET, greyscale=True, altLayout=True)
root = tdl.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="TURTLES",
                fullscreen=False)
tdl.setFPS(LIMIT_FPS)
con = tdl.Console(MAP_HEIGHT, MAP_WIDTH)
##################################################################


def count_turts():
    total = 0
    for y in range(MAP_HEIGHT - 1):
        for x in range(MAP_WIDTH - 1):
            cell = BOARD[x, y]
            if 2.0 <= cell <= 2.12 or 3.0 <= cell <= 3.12:
                total += 1

    print("There are %d total turtles" % (total))


BOARD[50, 50] = 2.0

turtlebrain = TurtleBrain(pattern='original')
timeprocessor = TimeStep(.01)
renderprocessor = RenderProcessor(con, root)

con.clear()


def handle_keys():

    user_input = tdl.event.key_wait()
    if user_input.key == 'ESCAPE':
        return 'exit'


    if user_input.key == 'ENTER':
        return 'go'

    if user_input.char == '2':
        return user_input.char

    if user_input.char == '3':
        return user_input.char


    if user_input.char == 'c':
        return 'clear'

    if user_input.char == 'p':
        return user_input.char

    if user_input.key == 'F11':
        return user_input.key



renderprocessor.process()

def add_turtle(turt):
    BOARD[randint(0, MAP_WIDTH - 1), randint(0, MAP_HEIGHT - 1)] = 2.0

while not tdl.event.is_window_closed():
    player_input = handle_keys()
    if player_input == 'go':
        turtlebrain.process()
        timeprocessor.process()
        renderprocessor.process()

    elif player_input == 'exit':
        break

    elif player_input == 'F11':
        tdl.set_fullscreen(not tdl.get_fullscreen())
        pass

    elif player_input == '2':
        add_turtle(2)
    elif player_input == '3':
        add_turtle(3)

    elif player_input == 'p':
        count_turts()


    elif player_input == 'clear':
        BOARD = np.zeros( (MAP_WIDTH, MAP_HEIGHT) )

    renderprocessor.process()
\$\endgroup\$
3
\$\begingroup\$

Here are few recommendations, in no particular order.

PEP8

You should consider formatting your code in accordance with pep8. This is important when sharing code, as the consistent style makes it much easier for other programmers to read your code.

There are various tools available to assist in making the code pep8 compliant. I use the PyCharm IDE which will show pep8 violations right in the editor.

Pylint

pylint can be used to help find code errors and non-best practice constructs. Here again PyCharm (and other IDEs) can run these tools on your code and show the results directly in the editor.

Try to simplify similar paths

In this code:

while self.pattern == 'original':
    if self.flag:
        self.move_twos()
        self.flag = (not self.flag)
        break
    else:
        self.move_threes()
        self.flag = (not self.flag)
        break        

Both if and else have a break, therefore the loop is unneeded. So it can be done more simply as:

if self.flag:
    self.move_twos()
else:
    self.move_threes()
self.flag = not self.flag

In addition, this loop:

failed = True
while True:
    dx = randint(-1, 1)
    dy = randint(-1, 1)
    new_x = x + dx
    new_y = y + dy
    if dx == 0 and dy == 0:
        continue
    else:
        failed = False
        break

can be simplified as:

while True:
    new_x = x + randint(-1, 1)
    new_y = y + randint(-1, 1)
    if x != new_x or y != newy:
        break

Calculate relationships, and use dict lookups:

Just about anytime you see a stacked if, in which the conditions and actions are basically the same, you should look to use a dict lookup instead.

So something this:

....
elif  2.0 <= BOARD[x, y] <= 2.01 or 3.0 <= BOARD[x, y] <= 3.01:
    self._con.draw_char(x, y, '0', fg=colors.dark_green, bg=colors.black)
elif 2.01 <= BOARD[x, y] <= 2.02 or 3.01 <= BOARD[x, y] <= 3.02:
    self._con.draw_char(x, y, '1', fg=colors.dark_lime, bg=colors.black)
elif 2.02 <= BOARD[x, y] <= 2.03 or 3.02 <= BOARD[x, y] <= 3.03:
    self._con.draw_char(x, y, '2', fg=colors.dark_red, bg=colors.black)
elif 2.03 <= BOARD[x, y] <= 2.04 or 3.03 <= BOARD[x, y] <= 3.04:
....

can likely be changed to something like (untested):

actions = {
    0: dict(char='0', fg=colors.dark_green, bg=colors.black),
    1: dict(char='1', fg=colors.dark_lime, bg=colors.black),
    2: dict(char='2', fg=colors.dark_red, bg=colors.black),
    ....
}
lookup = int((BOARD[x, y] - int(BOARD[x, y]) * 100)
self._con.draw_char(x, y, **actions[lookup])

This calculates a a lookup index into the actions dict and the gets the values for draw_char from there. The resulting construct will be much easier to read and modify.

\$\endgroup\$
  • \$\begingroup\$ Thank you! I knew there was room to simplify the loops and the if statements but the solutions went right over my head. Much appreciated. I'm pretty sure my nestedfor loops over a big board isn't the best way to do this, perhaps I can find a cleaner algorithm for the entire thing. \$\endgroup\$ – J-Sebastian-B Jun 11 '18 at 5:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.