Firstly, I wrote standard snake game, then I decided to increase difficulty and made game with various snake number. My main point was to try the curses module, the second - to create game with OOP architecture, maybe apply some design pattern.
Questions:
- Is
curses
usage optimal? - Should I split
game()
function some way (move some functionality out of there)? - Where is the better place for the
directions
dictionary -game()
function orSnake
class from the design point of view? - Do you see more suitable/beautiful architecture for this program? May be
Game
class should be added or something else.
Rules
- A player should manage multiple snakes by switching them by keyboard.
- Every snake has a name matched to key on keyboard: '1' -> 1, '2' -> 2, etc.
- Snake movements are controlled by arrow keys ↑, ↓, →,←.
- Snakes intersection allowed for simplicity.
The number and starting parameters of snakes are hard coded for simplicity.
import curses
from random import randint
class Snake:
def __init__(self, size, coords, g_win, color_pair_num):
y, x = coords
# The snake coordinates at the beginning
self.snk = [(y, x - i) for i in range(size)]
self.g_win = g_win
self.hght, self.wdth = g_win.getmaxyx()
self.key = curses.KEY_RIGHT
self.color_pair_num = color_pair_num
self.draw()
def draw(self):
for y, x in self.snk:
self.g_win.addch(y, x, curses.ACS_CKBOARD, curses.color_pair(self.color_pair_num))
self.g_win.refresh()
def move_head(self, directions):
y, x = self.snk[0]
dy, dx = directions[self.key]
y, x = y + dy, x + dx
# Move out of g_win
if y < 0 or y > self.hght or x < 0 or x > self.wdth:
return
# Bites itself
if (y, x) in self.snk[2:]:
return
# The reverse direction movement
if (y, x) == self.snk[1]:
self.snk = self.snk[::-1]
y, x = self.snk[0]
y, x = y + dy, x + dx
self.snk.insert(0, (y, x))
self.g_win.addch(y, x, curses.ACS_CKBOARD, curses.color_pair(self.color_pair_num))
return (y, x)
def move_tail(self):
tail_y, tail_x = self.snk.pop()
self.g_win.addch(tail_y, tail_x, ' ')
def change_active_snake(name, snakes, active, passive):
for num, snake in snakes.items():
color = active if num == name else passive
snake.color_pair_num = color
snake.draw()
def game(screen):
directions = {
curses.KEY_LEFT : (+0, -1),
curses.KEY_RIGHT : (+0, +1),
curses.KEY_UP : (-1, +0),
curses.KEY_DOWN : (+1, +0)
}
curses.use_default_colors()
curses.curs_set(0)
food_cnt = 0
speed = 1
hght, wdth = screen.getmaxyx()
score_win_width = 15
game_win_width = wdth - score_win_width
score_win = curses.newwin(hght, score_win_width, 0, game_win_width)
score_win.border()
game_win = curses.newwin(hght, game_win_width, 0, 0)
game_win.keypad(1)
game_win.border()
interval = 500
game_win.timeout(interval)
game_win.refresh()
score_win.refresh()
active = 1
passive = 2
curses.init_pair(active, curses.COLOR_RED, -1)
curses.init_pair(passive, curses.COLOR_BLUE, -1)
x = game_win_width // 2
snakes = {
1 : Snake(3, (int(hght * 0.2), x), game_win, active),
2 : Snake(3, (int(hght * 0.5), x), game_win, passive),
3 : Snake(3, (int(hght * 0.8), x), game_win, passive)
}
food = None
snk_num = 1
while True:
score_win.addstr(1, 1, f"Score: {food_cnt}")
score_win.addstr(2, 1, f"Speed: {speed}")
score_win.refresh()
for snk in snakes.values():
if not food:
y = randint(1, hght - 2)
x = randint(1, game_win_width - 2)
game_win.addch(y, x, curses.ACS_CKBOARD)
food = (y, x)
head = snk.move_head(directions)
if head == None:
return
if head == food:
food_cnt += 1
if food_cnt % 5 == 0:
interval = int(interval * 0.7)
game_win.timeout(interval)
speed += 1
food = None
else:
snk.move_tail()
new_key = game_win.getch()
if new_key in directions:
snakes[snk_num].key = new_key
# 48 is the decimal ascii code of '0'
# '1' is 49, '2' is 50, etc, so subtract 48
# to get snake name from new_key.
elif new_key - 48 in snakes.keys():
snk_num = new_key - 48
change_active_snake(snk_num, snakes, active, passive)
game_win.refresh()
curses.wrapper(game)
Appearance