diff --git a/run b/run index 27e40b1..923d2de 100755 --- a/run +++ b/run @@ -1 +1,4 @@ -gsettings set org.gnome.desktop.interface monospace-font-name 'square 13' && python src/main.py && gsettings set org.gnome.desktop.interface monospace-font-name 'Ubuntu Mono 13' +#! /usr/bin/bash +gsettings set org.gnome.desktop.interface monospace-font-name 'square 13' +python src/main.py +gsettings set org.gnome.desktop.interface monospace-font-name 'Ubuntu Mono 13' diff --git a/src/Apple.py b/src/Apple.py index daa4a09..20e6b54 100644 --- a/src/Apple.py +++ b/src/Apple.py @@ -1,13 +1,53 @@ import numpy as np +from src.utils import log + class Apple: - def __init__(self, y, x, h, w): + """ + The apple in the Snake game. + """ + def __init__(self, asset='o'): + """ + Creates a new empty apple. + + Args + ---- + asset : character + the character used to display the apple. + """ self.pos = None - self.spawn(y, x, h, w) + self.asset = asset def spawn(self, y, x, h, w): + """ + Spawns the apple inside the game area. + + Args + ---- + y : int + the y coordinate of the top left corner of the game area. + x : int + the x coordinate of the top left corner of the game area. + h : int + the height of the game area. + w : int + the width of the game area. + """ self.pos = (np.random.randint(y+1, y + h - 1), np.random.randint(x+1, x + w - 1)) + log(self.pos) def show(self, stdscr): - stdscr.addstr(*self.pos, 'O') + """ + Displays the apple on the screen. + + Args + ---- + stdscr : _curses.window + the window on which to display the object. + + Returns + ------- + None + """ + stdscr.addstr(*self.pos, self.asset) diff --git a/src/Snake.py b/src/Snake.py index e790a0b..f37b5a7 100644 --- a/src/Snake.py +++ b/src/Snake.py @@ -1,10 +1,11 @@ import curses +import random _directions = { curses.KEY_UP: (-1, 0), curses.KEY_DOWN: (1, 0), curses.KEY_LEFT: (0, -1), - curses.KEY_RIGHT: (0, 1) + curses.KEY_RIGHT: (0, 1), } _corners = { @@ -16,107 +17,228 @@ class Snake: - def __init__(self, y, x, h, w): - head = (y + (y + h) // 2, x + (x + w) // 2) - self.body = [head] - tmp = [tuple(map(sum, zip(head, (0, i)))) for i in range(1, 2)] - self.body += tmp - self.tokens = ['@'] + ['-'] * len(tmp) + """ The snake in the game of snake.""" + + NOTHING = 0 + SELF_BITE = -1 + OUTSIDE = -2 - self.dir = curses.KEY_LEFT - self.turned = '' + def __init__(self): + # the body and the assets (resp. the positions of the parts and the assets used for rendering) + self.body, self.assets = None, None + # a cache of the scene used in the game. + self.scene = None + # the direction in which the snake is going and the new asset to add to the snake. + self.dir, self.neck_asset = None, None + # a list containing parts to add to the snake, allows better animation. self.trail = [] - self.score = 0 + def spawn(self, y, x, h, w, init_length=3): + """ + Spawns the apple inside the game area. + + Args + ---- + y : int >= 0 + the y coordinate of the top left corner of the game area. + x : int >= 0 + the x coordinate of the top left corner of the game area. + h : int >= 0 + the height of the game area. + w : int >= 0 + the width of the game area. + init_length : int >= 0, optional + the initial length of the snake. + + """ + # cache the scene. self.scene = y, x, h, w - def change_direction(self, dir): - if (dir in [curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT] - and not (self.dir == curses.KEY_UP and dir == curses.KEY_DOWN) - and not (self.dir == curses.KEY_DOWN and dir == curses.KEY_UP) - and not (self.dir == curses.KEY_RIGHT and dir == curses.KEY_LEFT) - and not (self.dir == curses.KEY_LEFT and dir == curses.KEY_RIGHT)): - if dir != self.dir: - if self.dir == curses.KEY_RIGHT and dir == curses.KEY_UP: - self.turned = _corners["dr"] - elif self.dir == curses.KEY_RIGHT and dir == curses.KEY_DOWN: - self.turned = _corners["ur"] - - elif self.dir == curses.KEY_UP and dir == curses.KEY_RIGHT: - self.turned = _corners["ul"] - elif self.dir == curses.KEY_UP and dir == curses.KEY_LEFT: - self.turned = _corners["ur"] - - elif self.dir == curses.KEY_LEFT and dir == curses.KEY_UP: - self.turned = _corners["dl"] - elif self.dir == curses.KEY_LEFT and dir == curses.KEY_DOWN: - self.turned = _corners["ul"] - - elif self.dir == curses.KEY_DOWN and dir == curses.KEY_LEFT: - self.turned = _corners["dr"] - elif self.dir == curses.KEY_DOWN and dir == curses.KEY_RIGHT: - self.turned = _corners["dl"] - - self.dir = dir + # spawn in the middle of the scene, with random direction. + head = (y + h // 2, x + w // 2) + self.body = [head] + self.dir = random.sample([curses.KEY_LEFT, curses.KEY_DOWN, curses.KEY_UP, curses.KEY_RIGHT], k=1)[0] + + # build the initial body in the opposite direction. + if self.dir == curses.KEY_LEFT: + tmp = [tuple(map(sum, zip(head, (0, i + 1)))) for i in range(init_length)] + elif self.dir == curses.KEY_RIGHT: + tmp = [tuple(map(sum, zip(head, (0, -i - 1)))) for i in range(init_length)] + elif self.dir == curses.KEY_UP: + tmp = [tuple(map(sum, zip(head, (i + 1, 0)))) for i in range(init_length)] + else: + tmp = [tuple(map(sum, zip(head, (-i - 1, 0)))) for i in range(init_length)] + self.body += tmp + + # the assets are simply the head plus the right amount of body parts. + self.assets = ['@'] + ['|' if self.dir in [curses.KEY_UP, curses.KEY_DOWN] else '-'] * len(tmp) + + def change_direction(self, new_dir): + """ + Changes the direction of the snake if it is different and compatible with current one, + e.g. can not go from up to down without twisting the neck of the snake... + + Args + ---- + new_dir : int + the new direction that the player just gave as an input. + + Returns + ------- + None + """ + # useless if directions are the same. + if new_dir != self.dir: + # the snake turns only if: + # - the new direction is an arrow (1st line) + # - the player does not query a complete direction flip (4 last lines) + if (new_dir in [curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT] + and not (self.dir == curses.KEY_UP and new_dir == curses.KEY_DOWN) + and not (self.dir == curses.KEY_DOWN and new_dir == curses.KEY_UP) + and not (self.dir == curses.KEY_RIGHT and new_dir == curses.KEY_LEFT) + and not (self.dir == curses.KEY_LEFT and new_dir == curses.KEY_RIGHT)): + # check all possible combinations to determine the corner to display. + if self.dir == curses.KEY_RIGHT and new_dir == curses.KEY_UP: + self.neck_asset = _corners["dr"] # down+right corner. + elif self.dir == curses.KEY_RIGHT and new_dir == curses.KEY_DOWN: + self.neck_asset = _corners["ur"] # up+right corner. + + elif self.dir == curses.KEY_UP and new_dir == curses.KEY_RIGHT: + self.neck_asset = _corners["ul"] # up+left corner. + elif self.dir == curses.KEY_UP and new_dir == curses.KEY_LEFT: + self.neck_asset = _corners["ur"] # up+right corner. + + elif self.dir == curses.KEY_LEFT and new_dir == curses.KEY_UP: + self.neck_asset = _corners["dl"] # down+left corner. + elif self.dir == curses.KEY_LEFT and new_dir == curses.KEY_DOWN: + self.neck_asset = _corners["ul"] # up+left corner. + + elif self.dir == curses.KEY_DOWN and new_dir == curses.KEY_LEFT: + self.neck_asset = _corners["dr"] # down+right corner. + elif self.dir == curses.KEY_DOWN and new_dir == curses.KEY_RIGHT: + self.neck_asset = _corners["dl"] # down+left corner. + + self.dir = new_dir + + # return immediately. + return + + # here, no changes or going in a straight line -> update the asset accordingly. + if self.dir in [curses.KEY_UP, curses.KEY_DOWN]: + self.neck_asset = '|' + elif self.dir in [curses.KEY_LEFT, curses.KEY_RIGHT]: + self.neck_asset = '-' + else: + self.neck_asset = '*' def move(self): + """ + Moves the snake around the scene according to the direction. + + Args + ---- + + Returns + ------- + None + """ + # pop the head. self.body.pop() - self.tokens.pop() + self.assets.pop() + + # compute and insert the new head and the neck asset. new_head = self.body[0][0] + _directions[self.dir][0], self.body[0][1] + _directions[self.dir][1] self.body.insert(0, new_head) - if self.turned: - self.tokens.insert(1, self.turned) - elif self.dir in [curses.KEY_UP, curses.KEY_DOWN]: - self.tokens.insert(1, '|') - elif self.dir in [curses.KEY_LEFT, curses.KEY_RIGHT]: - self.tokens.insert(1, '-') + self.assets.insert(1, self.neck_asset) + + # complete the snake with its trail. if self.trail: self.body.append(self.trail.pop()) - self.tokens.append('.') - - self.turned = '' - - def is_eating(self, apple): - eating_apple = self.body[0] == apple.pos - if eating_apple: - self.score += 1 - apple.spawn(*self.scene) - return eating_apple - - def head(self): + self.assets.append('.') + + def is_eating(self, apples): + """ + Checks if the snake ate any of the apples in the scene. + + Args + ---- + apples : list of Apple instances + all the apples to check. + + Returns + ------- + count : int + the number of apples that the snake ate during current frame. + """ + count = 0 + for apple in apples: + eaten = self.body[0] == apple.pos # apple eaten? + if eaten: + curses.beep() # make a sound. + apple.spawn(*self.scene) # respawn the eaten apple. + count += eaten + return count + + def get_head(self): + """ Gives the head (int x int) of the snake. """ return self.body[0] def inside(self, y, x, h, w): + """ Checks whether the head of the snake is strictly inside the scene. """ return (y < self.body[0][0] < y + h - 1) and (x < self.body[0][1] < x + w - 1) def self_intersect(self): - return sum([self.head() == part for part in self.body[1:]]) - - def update(self, eating_apple, ): - if eating_apple: - self.trail += [self.body[-1]] * 10 + """ Returns a boolean telling if the snake bit its own tail. """ + # count the number of self intersections between the head and any of the body parts. + return sum([self.body[0] == part for part in self.body[1:]]) + + def update(self, eaten_apples, trail_length=10): + """ + Updates the snake. + + Args + ---- + eaten_apples : int + the number of apples that were eaten during current frame. + trail_length : int >= 1, optional + the number of body parts to append to the snake when an apple is eaten. Usually, depends on the + difficulty. + + Returns + ------- + score or error code: int + the score performed or an error code. + """ + if eaten_apples: + self.trail += [self.body[-1]] * trail_length # copy the tail a given amount of times, in place. + return eaten_apples if not self.inside(*self.scene): - return 1 - # raise Warning(f"hit walls -> {self.score}") + return Snake.OUTSIDE if self.self_intersect(): - return 2 - # raise Warning(f"self_intersect -> {self.score}") + return Snake.SELF_BITE - return 0 + return Snake.NOTHING def show(self, stdscr): + """ + Displays the snake on the screen. + + Args + ---- + stdscr : _curses.window + the window on which to display the object. + + Returns + ------- + None + """ + # show the parts with the right assets. for i, part in enumerate(self.body[1:]): - stdscr.addch(*part, self.tokens[i + 1]) + stdscr.addch(*part, self.assets[i + 1]) + # show the head only if inside the scene. if self.inside(*self.scene): stdscr.addstr(*self.body[0], '@') - - msgs = [f"trail: {self.trail}", - f" body: {len(self.body)}", - f"score: {self.score}"] - h = 0 if self.head()[0] > curses.LINES // 2 else curses.LINES - 1 - len(msgs) - for row, msg in zip(range(len(msgs)), list(map(str, msgs))): - stdscr.addstr(h + row, 0, msg) diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..a07468f --- /dev/null +++ b/src/utils.py @@ -0,0 +1,19 @@ +def log(*args, sep=' ', end='\n'): + """ + Logs a list of elements inside the 'log.log' file. + + Args + ---- + args : list of anything with a str method + all the elements to log into 'log.log' + sep : str + the separator between each element. + end : str + the end of the final string that will be logged. + + Returns + ------- + None + """ + with open("log.log", 'a') as file: + file.write(sep.join(map(str, args)) + end)