6
\$\begingroup\$

I've been working on a two player snake game in python. I would really appreciate any suggestions/general input that would help improve my coding.

"""
5-13-2018
Nathan van 't Hof
2 player snake game
players can move around using wasd (player 1) and ijkl (player 2)
eating apples: - will increase players length
"""

import random
import keyboard
import os
import colorama
import time

def get_apple(width, height, snake1, snake2):
    """
    find new random coordinate to place the apple
    """
    if len(snake1) + len(snake2) >= width*height:
        u = raw_input('You win!')
        quit()
    while True:
        apple = [random.randint(0, width - 1), random.randint(0, height-1)]
        if apple not in snake1 and apple not in snake2:
            return apple


def draw(game_field, i, key):
    """
    change a specific coordinate of the game field
    to a different type
    """
    game_field[i[0]][i[1]] = key
    return game_field


def move_snake(game_field, snake, typ):
    """
    move the snake one step
    """
    game_field = draw(game_field, snake[-2], typ)
    game_field = draw(game_field, snake[-1], 'O')
    return game_field


def change_pos(prev, width, height, dx, dy):
    """
    change the coordinate of the head of the snake
    """
    return [(prev[0]+dx)%width, (prev[1]+dy)%height]


def print_game(game_field):
    """
    print the game (in a readable format)
    """
    output = '.' * ((len(game_field)) * 2 + 1) + '\n'
    for i in game_field:
        output += '.' + ' '.join(i) + '.' + '\n'
    output += '.' * ((len(game_field)) * 2 + 1)
    # moves the marker to the top to prevent flikkering
    print('\033[H' + output)


def check_die(snake1, snake2):
    """
    check whether the snakes 'die'
    by letting the head bump into a tail/other head
    """
    dead = False
    if snake1[-1] == snake2[-1]:
        u = raw_input('Both snakes died')
        dead = True
    elif snake1[-1] in snake2 or snake1.count(snake1[-1]) >= 2:
        u = raw_input('Snake 1 died')
        dead = True
    elif snake2[-1] in snake1 or snake2.count(snake2[-1]) >= 2:
        u = raw_input('Snake 2 died')
        dead = True
    if dead:
        quit()


def check_movement(dx, dy, ch1, ch2, ch3, ch4):
    """
    check where the snake moves
    """
    if keyboard.is_pressed(ch1) and dx != -1 and dy != 0:
        return -1, 0
    if keyboard.is_pressed(ch2) and dx != 0 and dy != -1:
        return 0, -1
    if keyboard.is_pressed(ch3) and dx != 1 and dy != 0:
        return 1, 0
    if keyboard.is_pressed(ch4) and dx != 0 and dy != 1:
        return 0,1
    return dx, dy


def update_snake(new_pos, apple, game_field, snake1, snake2):
    snake1.append(new_pos)
    if new_pos == apple:
        apple = get_apple(width, height, snake1, snake2)
        game_field = draw(game_field, apple, '-')
    else:
        game_field = draw(game_field, snake1[0], ' ')
        del snake1[0]
    return snake1, apple, game_field


# init
width, height = 20, 20
snake1 = [[0,0],[0,1]]
snake2 = [[width/2, 0], [width/2 + 1, 0]]

apple = get_apple(width, height, snake1, snake2)
dx1, dy1 = 0, 1
dx2, dy2 = 0, 1

os.system('cls' if os.name == 'nt' else 'clear')
# this allows '\033[H' to work
colorama.init()

# draw inital positions of the field
game_field = [height*[' '] for i in range(width)]
for i in snake1:
    game_field = draw(game_field, i, 'x')
for i in snake2:
    game_field = draw(game_field, i, '+')

game_field = draw(game_field, snake1[-1], 'O')
game_field = draw(game_field, snake2[-1], 'O')
game_field = draw(game_field, apple, '-')

prev_time = time.time()
while True:
    try:
        fps = float(input('What framerate would you like to run at?'))
        break
    except:
        print('Please input a number')

os.system('cls' if os.name == 'nt' else 'clear')

while True:
    # check inputs from players
    dx1, dy1 = check_movement(dx1, dy1, 'w', 'a', 's', 'd')
    dx2, dy2 = check_movement(dx2, dy2, 'i', 'j', 'k', 'l')

    # update screen if enough time has passed
    if time.time() - prev_time > 1./fps:
        prev_time = time.time()
        print_game(game_field)
        new_pos1 = change_pos(snake1[-1], width, height, dx1, dy1)
        new_pos2 = change_pos(snake2[-1], width, height, dx2, dy2)

        # update snakes and playing field
        check_die(snake1, snake2)

        snake1, apple, game_field = update_snake(new_pos1, apple, game_field, snake1, snake2)
        snake2, apple, game_field = update_snake(new_pos2, apple, game_field, snake2, snake1)

        game_field = move_snake(game_field, snake1, 'x')
        game_field = move_snake(game_field, snake2, '+')
\$\endgroup\$
4
\$\begingroup\$

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-left of the terminal (\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 (diff).

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.

\$\endgroup\$
3
  • \$\begingroup\$ @Nathan I am glad that you appreciated it. Have you found the gist with the revisions useful? \$\endgroup\$ – metatoaster May 17 '18 at 2:58
  • \$\begingroup\$ Definitely, showing exactly what you mean makes understanding the differences really easy. I'll be sure to start using git as well. \$\endgroup\$ – Nathan May 17 '18 at 5:53
  • \$\begingroup\$ @Nathan Excellent! \$\endgroup\$ – metatoaster May 17 '18 at 5:53

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.