0

My program exits after a certain amount of time for no apparent reason. It doesn't throw any errors, and the requirement for the only time I call done() is not met.

(Sometimes the requirement for done() is met before the time the program exits out comes).

My code is below:

import random
from turtle import *
import sys


sys.setrecursionlimit(10000)
l = 0
defense_x1 = -200
defense_x2 = -200
defense_x3 = -200
defense_x4 = -200
zombies = []
defenses = []
attacks = []
placement_options = [0, 1, 2, -1]

def new_zombie():
    placement_level = random.choice(placement_options)
    z = vector(200, placement_level*100)
    print(placement_level)
    zombies.append(z)

def new_defense():
  global defense_x1
  global defense_x2
  global defense_x3
  global defense_x4
  i = input("select a number 1-4 on the keyboard. ")
  if i == "1":
    c = vector(defense_x1, 200)
    goto(c.x, c.y)
    dot(20, 'green')
    defense_x1 += 21
  elif i == "2":
    c = vector(defense_x2, 100)
    goto(c.x, c.y)
    dot(20, 'green')
    defense_x2 += 21
  elif i == "3":
    c = vector(defense_x3, 0)
    goto(c.x, c.y)
    dot(20, 'green')
    defense_x3 += 21
  elif i == "4":
    c = vector(defense_x4, -100)
    goto(c.x, c.y)
    dot(20, 'green')
    defense_x4 += 21
  defenses.append(c)

def shooting():
  for defense in defenses: 
    d = vector(defense.x, defense.y)
    goto(d.x, d.y)
    dot(5, 'blue')
    attacks.append(d)

def contact():
  for z in zombies:
    for a in attacks:
      if a.x >= z.x and a.y == z.y:
        zombies.remove(z)
        attacks.remove(a)
      elif a.x > 200:
          attacks.remove(a)

def drawing():
  clear()
  for zombie in zombies:
    goto(zombie.x, zombie.y)
    dot(20, 'red')
  for defense in defenses:
    goto(defense.x, defense.y)
    dot(20, 'green')
  for attack in attacks:
    goto(attack.x, attack.y)
    dot(5, 'blue')
  update()

def movement():
  global l
  for zombie in zombies:
    zombie.x -= 0.1
  drawing()
  contact()
  for z in zombies:
    if z.x < -200:
      done()
  for a in attacks:
    if a.x > 200:
      attacks.remove(a)
    a.x += 0.5
  l += 1
  if l == 1300:
    print("Firing Weapons!")
    l = 0
    shooting()
  if random.randint(1, 350) == 2:
    new_zombie()
  if random.randint(1, 900) == 2:
    print("You can build another defense module!")
    new_defense()

  movement()

def gameplay():
  setup(420, 420, 370, 0)
  hideturtle()
  tracer(False)
  up()
  new_zombie()
  movement()

gameplay()

and the code vector uses is below here, just in case you wanted to take a look.

import collections.abc
import math
import os

def path(filename):
    filepath = os.path.realpath(__file__)
    dirpath = os.path.dirname(filepath)
    fullpath = os.path.join(dirpath, filename)
    return fullpath

def line(a, b, x, y):
    turtle.up()
    turtle.goto(a, b)
    turtle.down()
    turtle.goto(x, y)

class vector(collections.abc.Sequence):
    precision = 6
    __slots__ = ('_x', '_y', '_hash')

    def __init__(self, x, y):
        self._hash = None
        self._x = round(x, self.precision)
        self._y = round(y, self.precision)

    @property

    def x(self):
        return self._x

    @x.setter

    def x(self, value):
        if self._hash is not None:
            raise ValueError('Cannot set x after hashing')
        self._x = round(value, self.precision)

    @property

    def y(self):
        return self._y

    @y.setter

    def y(self, value):
        if self._hash is not None:
            raise ValueError('Cannot set y after hashing')
        self._y = round(value, self.precision)

    def __hash__(self):
        #v.__hash__() -> hash(v)
        if self._hash is None:
            pair = (self.x, self.y)
            self._hash = hash(pair)

        return self._hash

    def __len__(self):
        return 2

    def __getitem__(self, index):
        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        else:
            raise IndexError

    def copy(self):
        type_self = type(self)
        return type_self(self.x, self.y)

    def __eq__(self, other):
        if isinstance(other, vector):
            return self.x == other.x and self.y == other.y
        return NotImplemented

    def __ne__(self, other):
        if isinstance(other, vector):
            return self.x != other.x and self.y != other.y
        return NotImplemented

    def __iadd__(self, other):
        if self._hash is not None:
            raise ValueError("Cannot add vector after hashing.")
        elif isinstance(other, vector):
            self.x += other.x
            self.y += other.y
        else:
            self.x += other
            self.y += other
        return self

    def __add__(self, other):
        copy = self.copy()
        return copy.__iadd__(other)

    __radd__ = __add__

    def move(self, other):
        self.__iadd__(other)

    def __isub__(self, other):
        if self._hash is not None:
            raise ValueError("Cannot subtract vector after hashing.")
        elif isinstance(other, vector):
            self.x -= other.x
            self.y -= other.y
        else:
            self.x -= other
            self.y -= other
        return self

    def __sub__(self, other):
        copy = self.copy()
        return copy.__isub__(other)

    def __imul__(self, other):
        if self._hash is not None:
            raise ValueError("Cannot multiply vector after hashing.")
        elif isinstance(other, vector):
            self.x *= other.x
            self.y *= other.y
        else:
            self.x *= other
            self.y *= other
        return self

    def __mul__(self, other):
        copy = self.copy()
        return copy.__imul__(other)

    __rmul__ = __mul__

    def scale(self, other):
        self.__imul__(other)

    def __itruediv__(self, other):
        if self._hash is not None:
            raise ValueError("Cannot divide vector after hashing.")
        elif isinstance(other, vector):
            self.x /= other.x
            self.y /= other.y
        else:
            self.x /= other
            self.y /= other
        return self

    def __truediv__(self, other):
        copy = self.copy()
        return copy.__itruediv__(other)

    def __neg__(self):
        copy = self.copy()
        copy.x = -copy.x
        copy.y = -copy.y
        return copy

    def __abs__(self):
        return (self.x**2+self.y**2)**0.5

    def rotate(self, angle):
        if self._hash is not None:
            raise ValueError("Cannot rotate vector after hashing.")
        radians= angle*math.pi/180.0
        cosine = math.cos(radians)
        sine = math.sin(radians)
        x = self.x
        y = self.y
        self.x = x*cosine - y*sine
        self.y = y*cosine + x*sine

    def __repr__(self):
        type_self = type(self)
        name = type_self.__name__
        return '{} ({!r},{!r})'.format(name, self.x, self.y)

Any and all help is appreciated, and other insight into functions or code I might not be using correctly is also welcome. Thanks for your time!

Frasher Gray
  • 222
  • 2
  • 14
  • 1
    So much code.. what's a _minimal_ program that "exits for no apparent reason"? In addition to make such questions much more focused and suitable for SO, this process of decomposing a problem (that is, isolating parts which are related from those that are not) is an important skill to learn for debugging. – user2864740 Jun 15 '20 at 17:46
  • Anyway, as this program uses _infinite_ recursion for a loop (see `movement`), it will fail with a Stack Overflow "at some point". Usually more than a few thousand iterations .. less than a million. – user2864740 Jun 15 '20 at 17:48
  • Is there any way to avoid it ever failing? I used to have ```ontimer(movement(), 50)``` , but I removed it in an attempt to figure out what was causing the failure. – Frasher Gray Jun 15 '20 at 17:50
  • Depends what "ever failing" means :| – user2864740 Jun 15 '20 at 17:50
  • I mean if you managed to keep the zombies from reaching the end, you could play it for the rest of your computer's life. – Frasher Gray Jun 15 '20 at 17:51
  • 1
    Also, I believe your second comment might underline its reason for failure, as it does fail after several thousand iterations. – Frasher Gray Jun 15 '20 at 17:52
  • 1
    A verification of the hypothesis then might be: switch `movement` to use a `while True` loop instead of recursion (it might run forever..) – user2864740 Jun 15 '20 at 17:53
  • After significant testing, the ```while True:``` does not make a difference. – Frasher Gray Jun 15 '20 at 18:02
  • Hmm :| Does it change the time between "ending without apparent reason" or otherwise affect the behavior? Could a Zombie be winning? What is the memory usage of the program over time (could it fail on system OOM)? – user2864740 Jun 15 '20 at 18:08
  • No, I changed the spawn rates so that no zombie had a chance to win, but it still closed out. As for the time, it seems to be the same. Do I have to change the recursion limit to a higher value? – Frasher Gray Jun 15 '20 at 18:10
  • There _shouldn't_ be any recursion ^_^ In any case, the recursion depth is limited by stack size and size of each frame on the stack .. generally outside of program control. – user2864740 Jun 15 '20 at 18:11
  • I am sorry, I made a mistake. I meant recursion LIMIT. I edited my previous comment. – Frasher Gray Jun 15 '20 at 18:13
  • I'm not seeing this "recursion limit". Is there code around this "limit"? – user2864740 Jun 15 '20 at 18:20
  • ```sys.setrecursionlimit(10000)``` 6th line. – Frasher Gray Jun 15 '20 at 18:22
  • If it's failing because of a stack _overflow_, that's outside of Python - setrecursionlimit is checked _inside/by_ Python. So change it the other way: `sys.setrecursionlimit(1234)`, which should result in _Python's [recursion] limit being reached_, and thus raising a RecursionError, _before_ the process itself hits a guard page and rudely crashes. Anyway, if _not_ using [unbound] recursion - ie. switching to `while True: ..` - then none of this applies. – user2864740 Jun 15 '20 at 18:30
  • 1
    The default limit is 1000 and should rarely be changed - this only affects Python's check, which should be less than the process's maximum capabilities, and does not actually increase the process's stack size. If wishing to set the stack size as well, see https://stackoverflow.com/a/16248113/2864740 -- this will depend on OS (eg. is not supported in Windows), and I don't recommend such changes for general cases. – user2864740 Jun 15 '20 at 18:36

1 Answers1

2

Checked, it flash quit after called movement() 1469 times. Of course, I replaced input with str(random.randint(1, 4)) for no waiting and no user input.

It should be caused by recursive call. After following actions, it works fine.

  • Remove movement() call in function movement()
  • Replace movement() call in function gameplay() by
while True:
    movement()
Jason Yang
  • 11,284
  • 2
  • 9
  • 23