275 lines
6.8 KiB
Python
Executable File
275 lines
6.8 KiB
Python
Executable File
#!/bin/env python3
|
|
|
|
import abc
|
|
import argparse
|
|
import dataclasses
|
|
import math
|
|
import random
|
|
from typing import List
|
|
|
|
import pygame
|
|
|
|
|
|
# TODO replace with pygame.Rect
|
|
@dataclasses.dataclass
|
|
class Entity:
|
|
pos: pygame.Vector2
|
|
size: int
|
|
|
|
@property
|
|
def top(self) -> int:
|
|
return self.pos.y
|
|
|
|
@property
|
|
def bottom(self) -> int:
|
|
return self.pos.y + self.size
|
|
|
|
@property
|
|
def left(self) -> int:
|
|
return self.pos.x
|
|
|
|
@property
|
|
def right(self) -> int:
|
|
return self.pos.x + self.size
|
|
|
|
@abc.abstractmethod
|
|
def draw(self, screen: pygame.Surface) -> None:
|
|
pass
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Food(Entity):
|
|
color: str = "white"
|
|
growth: int = 5
|
|
size: int = 10
|
|
|
|
@classmethod
|
|
def new(cls):
|
|
return cls(
|
|
pos=pygame.Vector2(random.randint(0, sw), random.randint(0, sh)),
|
|
)
|
|
|
|
def draw(self, screen: pygame.Surface) -> None:
|
|
p = pygame.Vector2(self.pos.x + self.size / 2, self.pos.y + self.size / 2)
|
|
pygame.draw.circle(screen, self.color, p, self.size / 2)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Man(Entity):
|
|
name: str = None
|
|
speed: float = 500
|
|
color: str = "yellow"
|
|
alive: bool = True
|
|
size: int = 40
|
|
enabled: bool = True
|
|
|
|
key_up: int = pygame.K_w
|
|
key_down: int = pygame.K_s
|
|
key_left: int = pygame.K_a
|
|
key_right: int = pygame.K_d
|
|
|
|
def handle_input(self, inputs: pygame.key.ScancodeWrapper, dt: float) -> None:
|
|
if self.enabled:
|
|
horz, vert = 0, 0
|
|
if keys[self.key_up]:
|
|
vert = -1
|
|
if keys[self.key_down]:
|
|
vert = 1
|
|
if keys[self.key_left]:
|
|
horz = -1
|
|
if keys[self.key_right]:
|
|
horz = 1
|
|
self.walk(horz, vert)
|
|
|
|
def walk(self, horizontal, vertical):
|
|
horizontal = horizontal / abs(horizontal) if horizontal else 0
|
|
vertical = vertical / abs(vertical) if vertical else 0
|
|
mov = self.speed * dt
|
|
if horizontal != 0 and vertical != 0:
|
|
mov = math.sqrt(2 * mov**2) / 2
|
|
|
|
self.move(mov * horizontal, mov * vertical)
|
|
|
|
def move(self, dx: int, dy: int) -> None:
|
|
self.pos.x += dx
|
|
self.pos.y += dy
|
|
|
|
def draw(self, screen: pygame.Surface) -> None:
|
|
p = pygame.Vector2(self.pos.x + self.size / 2, self.pos.y + self.size / 2)
|
|
pygame.draw.circle(screen, self.color, p, self.size / 2)
|
|
|
|
def can_eat(self, other: Entity) -> bool:
|
|
overlaps_x = self.left <= other.left and self.right >= other.right
|
|
overlaps_y = self.top <= other.top and self.bottom >= other.bottom
|
|
return overlaps_x and overlaps_y and self.size >= other.size
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class AI:
|
|
man: Man = None
|
|
enemies: List[Man] = None
|
|
target = None
|
|
|
|
def decide(self):
|
|
if self.man is None:
|
|
return
|
|
|
|
target = self.target
|
|
if (target and target.size > self.man.size) or random.random() * 100 < 10:
|
|
target = None
|
|
|
|
if target is None:
|
|
targets = [
|
|
e for e in self.enemies or [] if e.alive and e.size < self.man.size
|
|
]
|
|
if targets:
|
|
target = random.choice(targets)
|
|
|
|
if target is None:
|
|
target = food
|
|
|
|
self.target = target
|
|
|
|
horz = (target.pos.x + target.size / 2) - (self.man.pos.x + self.man.size / 2)
|
|
vert = (target.pos.y + target.size / 2) - (self.man.pos.y + self.man.size / 2)
|
|
self.man.walk(horz, vert)
|
|
|
|
def hook(self, player):
|
|
player.enabled = False
|
|
self.man = player
|
|
self.target = None
|
|
self.enemies = [p for p in players if p != player]
|
|
|
|
def release(self):
|
|
self.man.enabled = True
|
|
self.man = None
|
|
self.target = None
|
|
self.enemies = players
|
|
|
|
|
|
# set up pygame
|
|
pygame.init()
|
|
pygame.display.set_caption("Maxman")
|
|
screen = pygame.display.set_mode([1000, 1000])
|
|
clock = pygame.time.Clock()
|
|
running = True
|
|
paused = True
|
|
font = pygame.font.Font("freesansbold.ttf", 32)
|
|
space_was_pressed = False
|
|
dt = 0
|
|
|
|
# set up board
|
|
sw, sh = screen.get_width(), screen.get_height()
|
|
maxman = Man(pygame.Vector2(sw / 4, sh / 4), name="Maxman")
|
|
blackman = Man(
|
|
pygame.Vector2(sw * 0.75, sh * 0.25),
|
|
name="Blackman",
|
|
color="black",
|
|
key_up=pygame.K_i,
|
|
key_down=pygame.K_k,
|
|
key_left=pygame.K_j,
|
|
key_right=pygame.K_l,
|
|
)
|
|
chrisman = Man(
|
|
pygame.Vector2(sw * 0.25, sh * 0.75),
|
|
name="Chrisman",
|
|
color="red",
|
|
key_up=pygame.K_UP,
|
|
key_down=pygame.K_DOWN,
|
|
key_left=pygame.K_LEFT,
|
|
key_right=pygame.K_RIGHT,
|
|
)
|
|
manman = Man(
|
|
pygame.Vector2(sw * 0.75, sh * 0.75),
|
|
name="Manman",
|
|
color="green",
|
|
key_up=pygame.K_KP_8,
|
|
key_down=pygame.K_KP_2,
|
|
key_left=pygame.K_KP_4,
|
|
key_right=pygame.K_KP_6,
|
|
)
|
|
players = [maxman, blackman, chrisman, manman]
|
|
food = Food.new()
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--ai",
|
|
type=int,
|
|
choices=[0, 1, 2, 3, 4],
|
|
default=1,
|
|
help="number of AI players",
|
|
)
|
|
args = parser.parse_args()
|
|
ais = []
|
|
for i in range(args.ai):
|
|
ai = AI()
|
|
ai.hook(players[-i - 1])
|
|
ais.append(ai)
|
|
|
|
while running:
|
|
# check recent events
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
running = False
|
|
|
|
# draw players in order of size
|
|
screen.fill("purple")
|
|
food.draw(screen)
|
|
for p in sorted(players, key=lambda p: p.size):
|
|
p.draw(screen)
|
|
|
|
keys = pygame.key.get_pressed()
|
|
if len(players) == 1:
|
|
text = font.render(
|
|
f"{players[0].name} wins",
|
|
True,
|
|
"black",
|
|
)
|
|
text_rect = text.get_rect()
|
|
screen.blit(
|
|
text,
|
|
pygame.Vector2((sw - text_rect.width) / 2, (sh - text_rect.height) / 2),
|
|
)
|
|
|
|
if keys[pygame.K_SPACE]:
|
|
running = False
|
|
|
|
else:
|
|
for p in players:
|
|
for op in players:
|
|
if p != op and p.can_eat(op):
|
|
op.alive = False
|
|
players.remove(op)
|
|
|
|
if p.can_eat(food):
|
|
p.size += food.growth
|
|
food = food.new()
|
|
|
|
# handle inputs
|
|
if paused:
|
|
text = font.render("Pause", True, "black")
|
|
text_rect = text.get_rect()
|
|
screen.blit(
|
|
text,
|
|
pygame.Vector2((sw - text_rect.width) / 2, (sh - text_rect.height) / 2),
|
|
)
|
|
|
|
else:
|
|
for ai in ais:
|
|
ai.decide()
|
|
|
|
for p in players:
|
|
p.handle_input(keys, dt)
|
|
|
|
if keys[pygame.K_SPACE]:
|
|
if not space_was_pressed:
|
|
paused = not paused
|
|
space_was_pressed = True
|
|
else:
|
|
space_was_pressed = False
|
|
|
|
pygame.display.flip()
|
|
dt = clock.tick(100) / 1000
|
|
|
|
pygame.quit()
|