Skip to main content
added 140 characters in body
Source Link

That clears the screen (\033[1J) and place the cursor on top-left of the fileterminal (\033[1;1H). (diff)

Also the framerate conversion from input, for that matter any use of except should only trap the Exception class that is expected. Having it like that means the user cannot break out of the program with Ctrl-C even though they might want to terminate the program. It should only trap the ValueError exception (diff).

That clears the screen (\033[1J) and place the cursor on top of the file (\033[1;1H). (diff)

Also the framerate conversion from input, for that matter any use of except should only trap the Exception class that is expected. Having it like that means the user cannot break out of the program with Ctrl-C even though they might want to terminate the program. It should only trap the ValueError exception.

That clears the screen (\033[1J) and place the cursor on top-left of the terminal (\033[1;1H). (diff)

Also the framerate conversion from input, for that matter any use of except should only trap the Exception class that is expected. Having it like that means the user cannot break out of the program with Ctrl-C even though they might want to terminate the program. It should only trap the ValueError exception (diff).

Source Link

First impressions:

Good work on the multiple platform attempt, however given that you are already using colorama and raw ansi codes (i.e. \033[H), there exist one for clearing the screen which should be used instead of os.system. Also while at it, avoid duplicating code, i.e. wrap repeated calls in a function, something like:

def clear_screen():
    print('\033[1J\033[1;1H')

That clears the screen (\033[1J) and place the cursor on top of the file (\033[1;1H). (diff)

The other thing that should be done is wrap the main game in a function (from # init and down), and call it inside the if __name__ == '__main__': section.

However, once moving everything inside we find that the update_snake function is now broken. This is caused by that function not actually taking in the width and height arguments like you had structure your program. Not a big deal, just add them as appropriate (so that it is not coupled to the value defined for the module):

def update_snake(new_pos, apple, game_field, width, height, snake1, snake2):

Fix the function call as appropriate with the same signature. I just followed the ordering that you used for get_apple. (diff)

Another thing that jumped out (when running this under Linux) is that the root user is required for the keyboard module. This might be why the question didn't really get looked at, as that's the administrator account. The curses module may be more appropriate, however Windows user will require an additional wheel to be installed. As a bonus, ncurses provides a lot more flexibility with drawing on the terminal, it is something good to get familiar with. (doing cross-platform terminal interaction (or anything) can be surprisingly painful, so this is not a fault of your program, rather, I have to commend you for trying).

You had used print as a function. Good, this brings up the Python 3 compatibility. However, this does not work under Python 3 as one might expect. First, width / 2 has the / (division) operator which no longer return an int if given floats as arguments. The // operator or the floor division operator should be used instead to get int for sure.

snake2 = [[width//2, 0], [width//2 + 1, 0]]

The other bit that could be problematic is towards the end of the game, where the call to raw_input crashes the game under Python 3. That is changed and since you are simply outputting a string, just use a print statement, and perhaps an input('') call at the end of that. While at it, normalise input to raw_input for Python 2 with a statement like this (diff):

import sys

if sys.version_info < (3,):
    input = raw_input

I also wouldn't use quit() as that's for the interactive shell, you should use sys.exit instead. Ideally, random calls to abort the program shouldn't be invoked like so, but I am going to let this slide for now.

Also the framerate conversion from input, for that matter any use of except should only trap the Exception class that is expected. Having it like that means the user cannot break out of the program with Ctrl-C even though they might want to terminate the program. It should only trap the ValueError exception.

While at that, we can also trap that on the main function, so invoking it might look like:

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print('user exit')
        sys.exit(1)

Although the keyboard library underneath has threads that might not terminate nicely like this, there is probably documentation in there that might reveal how that might be fixed, but in general this is a way to ensure that when user Ctrl-C on a program to terminate it, rather than showing them a big long traceback, a simple message will suffice.

The code is pretty nicely formatted, almost fully PEP-8 compliant. Just remember to space out the symbols and operators for equations to improve readability.

Other improvements I would do, but we are starting to make actual logic changes to the program. I had mentioned sprinkling random exits in functions isn't a good thing, as that needlessly couple the game logic to program lifecycle. Let's change the check_die function to return the exit string if a death happened, and an empty string if no death happened.

def check_die(snake1, snake2):
    if snake1[-1] == snake2[-1]:
        return 'Both snakes died'
    # and so on ...

In the game loop (diff)

# update snakes and playing field
death = check_die(snake1, snake2)
if death:
    print(death)
    input('')  # capture input characters in buffer
    return

As for the get_apple, the while loop in there is going to be a massive liability for performance once you have really good players - the game will slow down as it tries to randomly find the last remaining empty spots. I would use random.choice on the tiles that are not currently occupied by a snake... while this map isn't available since empty tiles are not being tracked, generating that should be acceptable, though it will take some time, it may not matter too much. One last thing though, we can't exactly return a value for winning given how get_apple is used (two consecutive calls for each snake). One can use an Exception to signify that there is no valid spot remaining.

try:
    snake1, apple, game_field = update_snake(
        new_pos1, apple, game_field, width, height, snake1, snake2)
    snake2, apple, game_field = update_snake(
        new_pos2, apple, game_field, width, height, snake2, snake1)
except NoSpaceForApple:
    print('You win!')
    input('')  # capture input characters in buffer
    return

Finally, to better present what was done here, I created a gist that shows all the changes as a complete program. The reason why I used a gist is that the way they show revisions is very useful in showing how the corrections progressed. You may have noticed this already with the diff links that I've added, but you should be able to follow everything from bottom up, or git clone https://gist.github.com/7eb66780a852ee6e32862ba4ee896b61 should get you a copy. By the way, if you don't know git (or other version control) you should start using it, because you can make use of it like so to show how the program changes over time by the fixes done to it.

Verdict: This is a really neat little game, you really invoke my nostalgia (back when I wrote things in DOS) and I give you props for that.