from collections import defaultdict
from random import sample, shuffle
import time # clock
from copy import deepcopy
class Cardinal:
NORTH = 0
EAST = 1
SOUTH = 2
WEST = 3
class CellType:
BAD = -2
GOOD = -1
EMPTY = 0
SNAKE_1 = 1
SNAKE_2 = 2
SNAKE_3 = 3
SNAKE_4 = 4
def is_food(value):
return value < EMPTY
def is_snake(value):
return value > EMPTY
class FoodType:
RED = -2
BLUE = 3
def value_by_cell(cell):
if cell == CellType.GOOD:
return BLUE
else:
return RED
class ScoreTable:
def __init__(self, players):
self.games = 0
self.table = { player.id : self.__row(players) for player in players }
def __row(self, players):
return { 'wins' : 0, 'deaths' : 0, 'kills' : [0 for p in players] }
def score_win(self, player):
self.table[player]['wins'] += 1
def score_kill(self, killer, died):
self.table[killer]['kills'][died] += 1
self.table[died]['deaths'] += 1
def get_results(self):
return sorted(self.table.items(),
key = lambda (k,v):(v['wins'], sum(v['kills']) - v['deaths'],
reverse = True)
class Game:
SNAKE_MIN = 2
def __init__(self, game_id, speed, goodies, baddies, scoring, players):
self.id = game_id
self.speed = speed
self.scoring = scoring
self.players = players
self.snakes = []
self.food = []
self.board = Board(42, 42, goodies, baddies)
self.spawn_snakes()
self.spawn_food()
# game loop
while True:
alive = [snake for snake in self.snakes if snake.alive]
if len(alive) > 1:
self.tick(speed)
elif len(alive) == 1:
snake = alive[0]
# snake must stay alive for 10 more ticks
for i in xrange(10):
self.tick(speed)
if not snake.alive:
# no winner
snake = None
self.game_over(snake)
break
else:
self.game_over(None)
break
def spawn_snakes(self):
# snake parameters for each player (position, direction)
p1 = ((self.width // 4, self.height // 2), Cardinal.EAST)
p2 = ((self.width - p1[0], self.height - p1[1]), Cardinal.WEST)
p3 = ((self.width // 2, self.height // 4), Cardinal.SOUTH)
p4 = ((self.width - p3[0], self.height - p3[1]), Cardinal.NORTH)
# though there is no significant advantage, randomize starting positions anyway
params = shuffle([p1, p2, p3, p4])
# spawn a snake for each player
for i in xrange(len(self.players)):
snake = Snake(i+1, self.players[i], *params[i])
# give snakes an initial boost of 3
snake.feed(3)
self.snakes.append(snake)
for snake in self.snakes:
self.board.set_cell(snake.id, *snake.head())
def spawn_food(self):
# subtract current food count from limit
goodies = self.board.goodie_limit - len(self.board.dictionary[CellType.GOOD])
baddies = self.board.baddie_limit - len(self.board.dictionary[CellType.BAD])
sample_size = (goodies + baddies)
if sample_size == 0:
return
# get all empty cells
empty = self.board.dictionary[CellType.EMPTY]
if sample_size > len(empty):
# get the entire list of empty positions in random order
sample_size = len(empty)
positions = sample(empty, sample_size)
# target percentage of goodies
ratio = self.board.goodie_limit / float(self.board.goodie_limit + self.board.baddie_limit)
# spawn food such that the target ratio is approached
i = 0
while i < sample_size:
compare = cmp(goodies / float(goodies + baddies), baddies / float(goodies + baddies))
if compare < 0:
self.board.set_cell(FoodType.BLUE, *positions[i])
goodies -= 1
elif compare > 0:
self.board.set_cell(FoodType.RED, *positions[i])
baddies -= 1
else:
if randint(0,1) == 1:
self.board.set_cell(FoodType.BLUE, *positions[i])
goodies -= 1
else:
self.board.set_cell(FoodType.RED, *positions[i])
baddies -= 1
i += 1
else:
positions = sample(empty, sample_size)
# spawn maximum amount of food
for i in xrange(sample_size):
if i < goodies:
self.board.set_cell(FoodType.BLUE, *positions[i])
else:
self.board.set_cell(FoodType.RED, *positions[i])
def snake_choices(self):
# read-only copy of the game board for player access
_board = deepcopy(self.board)
for snake in self.snakes:
time_taken = time.clock()
# call each snake's choice logic
snake.choose_direction(_board, time_taken)
time_taken = time.clock() - time_taken
if time_taken > 1.0:
exit('Player %s exceeded the time limit, taking %s seconds.' % (snake.__class__.__name__, time_taken))
# TODO
def move_snakes(self):
# in the event that both snakes chose the same destination, one will move first,
# then the other will have a head-to-head collision, killing both
for snake in self.snakes:
if not snake.alive:
break
head_pos = snake.head()
move_to = self.board.get_coords(snake.direction, *head_pos)
cell_value = self.board.get_cell(*move_to)
if CellType.is_snake(cell_value):
# killed by enemy snake
snake.die()
enemy = self.snakes[cell_value - 1]
self.scoring.score_kill(enemy.id, snake.id)
if enemy.alive and move_to == enemy.head():
# head-to-head collision kills both
enemy.die()
self.scoring.score_kill(snake.id, enemy.id)
break
# did snake get food?
food_value = 0
if CellType.is_food(cell_value):
food_value = FoodType.value_by_cell(cell_value)
# move snake tail if not growing
if snake.growing <= 0:
# move tail
tail = snake.cells.pop()
self.board.set_cell(CellType.EMPTY, *tail)
if snake.growing < 0:
# shrink tail
tail = snake.cells.pop()
self.board.set_cell(CellType.EMPTY, *tail)
self.growing += 1
# move snake head
self.board.set_cell(snake.id, *move_to)
# feeding happens after moving so that growing isn't instant
if food_value:
snake.feed(food_value)
# TODO: score for length/eaten?
# spawn replacement food
self.spawn_food()
def tick(self):
self.snake_choices()
self.move_snakes()
# TODO: measure time taken?
def game_over(self, winner):
if winner:
self.scoring.score_win(winner.id)
print 'Game: %s, Winner: (%s) %s' % (self.id, winner.id, winner.__class__.__name__)
else:
print 'Game: %s, No winner' % self.id
class Board:
def __init__(self, width, height, goodie_limit, baddie_limit, torus = True):
self.width = width
self.height = height
self.goodie_limit = goodie_limit
self.baddie_limit = baddie_limit
# TODO: Not currently used
self.torus = torus
# 2D list
self.grid = [[CellType.EMPTY for x in xrange(self.width)] for y in xrange(self.height)]
# cells by type
self.dictionary = defaultdict(list, {CellType.EMPTY : [(x,y) for y in xrange(self.height) for x in xrange(self.width)]})
def get_cell(self, x, y):
return self.grid[y][x]
def set_cell(self, value, x, y):
self.dictionary[self.get_cell(x,y)].remove( (x,y) )
self.dictionary[value].push( (x,y) )
self.grid[y][x] = value
def get_coords(self, direction, x, y):
if direction == Cardinal.NORTH:
return (x, (y-1) % self.height)
if direction == Cardinal.EAST:
return ((x+1) % self.width, y)
if direction == Cardinal.SOUTH:
return (x, (y+1) % self.height)
if direction == Cardinal.WEST:
return ((x-1) % self.width, y)
raise ValueError('get_coords(%s, %s, %s)' % (direction, x, y))
def clear(self):
for y in xrange(self.height):
for x in xrange(self.width):
self.set_cell(x, y, CellType.EMPTY)
class Snake:
def __init__(self, snake_id, player_class, position, direction):
self.id = snake_id
self.player = player_class
self.cells = [position]
self.direction = direction
self.alive = True
self.growing = 0
def head(self):
return self.cells[0]
def length(self):
return len(self.cells)
def straight(self):
pass
def left(self):
self.direction = (self.direction - 1) % 4
def right(self):
self.direction = (self.direction + 1) % 4
def choose_direction(self, _board, time_stamp):
# Get move choice from self.player
self.player.choose_direction(_board, time_stamp)
assert(isinstance(player.direction, int))
assert(0 <= player.direction < 4)
self.direction = player.direction
def feed(self, value):
# RED does nothing when min length
if value < 0 and len(self.cells) <= Game.SNAKE_MIN:
self.growing = 0
return
self.growing += value
def die(self):
self.alive = False
self.growing = 0
# Essentially a read-only Snake class so that players may use the information
class Snake_ReadOnly:
def __init__(self, snake):
self.id = snake.id
self.cells = snake.cells[:]
self.direction = snake.direction
self.alive = snake.alive
self.growing = snake.growing
def head(self):
return self.cells[0]
def length(self):
return len(self.cells)
def straight(self):
return self.direction
def left(self):
return (self.direction - 1) % 4
def right(self):
return (self.direction + 1) % 4
class Player:
def __init__(self):
self.direction = None
# @board - a copy of the Board class
# @time_stamp - the time the clock started ticking for your move
def choose_direction(self, board, time_stamp):
pass
class RandomBot(Player):
def __init__(self):
from random import randint
def choose_direction(self, board, time_stamp):
self.direction = [me.left, me.straight, me.right][randint(0,2)]()
def main():
return
# game options
speed = 1
goodies = 100
baddies = 10
# instantiate player classes
players = [RandomBot(), RandomBot()]
# create score table
scoring = ScoreTable(players)
# TODO: Loop for multiple games
game = Game(i, speed, goodies, baddies, scoring, players)
main()