From eae47faed0344df7992d293609f5b055469f710f Mon Sep 17 00:00:00 2001 From: Maximilian Moser Date: Tue, 27 Feb 2024 23:20:48 +0100 Subject: [PATCH] maxman: add initial implementation --- Pipfile | 12 +++ Pipfile.lock | 84 ++++++++++++++++++ maxman.py | 237 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 333 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 maxman.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..984c980 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pygame = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..3be1e0c --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,84 @@ +{ + "_meta": { + "hash": { + "sha256": "88a6aed012a38fd06a01fb57f063f0e338b4d184b7c5c974a94737eb695ba85a" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "pygame": { + "hashes": [ + "sha256:03879ec299c9f4ba23901b2649a96b2143f0a5d787f0b6c39469989e2320caf1", + "sha256:074aa6c6e110c925f7f27f00c7733c6303407edc61d738882985091d1eb2ef17", + "sha256:0e24d05184e4195fe5ebcdce8b18ecb086f00182b9ae460a86682d312ce8d31f", + "sha256:1822d534bb7fe756804647b6da2c9ea5d7a62d8796b2e15d172d3be085de28c6", + "sha256:1a2a43802bb5e89ce2b3b775744e78db4f9a201bf8d059b946c61722840ceea8", + "sha256:1c289f2613c44fe70a1e40769de4a49c5ab5a29b9376f1692bb1a15c9c1c9bfa", + "sha256:1f3849f97372a3381c66955f99a0d58485ccd513c3d00c030b869094ce6997a6", + "sha256:224c308856334bc792f696e9278e50d099a87c116f7fc314cd6aa3ff99d21592", + "sha256:263b4a7cbfc9fe2055abc21b0251cc17dea6dff750f0e1c598919ff350cdbffe", + "sha256:2b34c73cb328024f8db3cb6487a37e54000148988275d8d6e5adf99d9323c937", + "sha256:30a8d7cf12363b4140bf2f93b5eec4028376ca1d0fe4b550588f836279485308", + "sha256:31648d38ecdc2335ffc0e38fb18a84b3339730521505dac68514f83a1092e3f4", + "sha256:34646ca20e163dc6f6cf8170f1e12a2e41726780112594ac061fa448cf7ccd75", + "sha256:35632035fd81261f2d797fa810ea8c46111bd78ceb6089d52b61ed7dc3c5d05f", + "sha256:35cf093a51cb294ede56c29d4acf41538c00f297fcf78a9b186fb7d23c0577b6", + "sha256:39690e9be9baf58b7359d1f3b2336e1fd6f92fedbbce42987be5df27f8d30718", + "sha256:3b3e619e33d11c297d7a57a82db40681f9c2c3ae1d5bf06003520b4fe30c435d", + "sha256:3b8a6e351665ed26ea791f0e1fd649d3f483e8681892caef9d471f488f9ea5ee", + "sha256:41f8779f52e0f6e6e6ccb8f0b5536e432bf386ee29c721a1c22cada7767b0cef", + "sha256:47a8415d2bd60e6909823b5643a1d4ef5cc29417d817f2a214b255f6fa3a1e4c", + "sha256:485239c7d32265fd35b76ae8f64f34b0637ae11e69d76de15710c4b9edcc7c8d", + "sha256:4f1559e7efe4efb9dc19d2d811d702f325d9605f9f6f9ececa39ee6890c798f5", + "sha256:4ff21201df6278b8ca2e948fb148ffe88f5481fd03760f381dd61e45954c7dff", + "sha256:5697528266b4716d9cdd44a5a1d210f4d86ef801d0f64ca5da5d0816704009d9", + "sha256:677e37bc0ea7afd89dde5a88ced4458aa8656159c70a576eea68b5622ee1997b", + "sha256:68c4e8e60b725ffc7a6c6ecd9bb5fcc5ed2d6e0e2a2c4a29a8454856ef16ad63", + "sha256:6cf2257447ce7f2d6de37e5fb019d2bbe32ed05a5721ace8bc78c2d9beaf3aee", + "sha256:6d58c8cf937815d3b7cdc0fa9590c5129cb2c9658b72d00e8a4568dea2ff1d42", + "sha256:6fe323acbf53a0195c8c98b1b941eba7ac24e3e2b28ae48e8cda566f15fc4945", + "sha256:74e1d6284100e294f445832e6f6343be4fe4748decc4f8a51131ae197dae8584", + "sha256:78fcd7643358b886a44127ff7dec9041c056c212b3a98977674f83f99e9b12d3", + "sha256:7d0a2794649defa57ef50b096a99f7113d3d0c2e32d1426cafa7d618eadce4c7", + "sha256:88d1cdacc2d3471eceab98bf0c93c14d3a8461f93e58e3d926f20d4de3a75554", + "sha256:9b30bc1220c457169571aac998e54b013aaeb732d2fd8744966cb1cfab1f61d1", + "sha256:9bd738fd4ecc224769d0b4a719f96900a86578e26e0105193658a32966df2aae", + "sha256:9dcff6cbba1584cf7732ce1dbdd044406cd4f6e296d13bcb7fba963fb4aeefc9", + "sha256:a0769eb628c818761755eb0a0ca8216b95270ea8cbcbc82227e39ac9644643da", + "sha256:a0bd67426c02ffe6c9827fc4bcbda9442fbc451d29b17c83a3c088c56fef2c90", + "sha256:bc12e4dea3e88ea8a553de6d56a37b704dbe2aed95105889f6afeb4b96e62097", + "sha256:c13edebc43c240fb0532969e914f0ccefff5ae7e50b0b788d08ad2c15ef793e4", + "sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a", + "sha256:ce4b6c0bfe44d00bb0998a6517bd0cf9455f642f30f91bc671ad41c05bf6f6ae", + "sha256:cf2191b756ceb0e8458a761d0c665b0c70b538570449e0d39b75a5ba94ac5cf0", + "sha256:d29a84b2e02814b9ba925357fd2e1df78efe5e1aa64dc3051eaed95d2b96eafd", + "sha256:d75cbbfaba2b81434d62631d0b08b85fab16cf4a36e40b80298d3868927e1299", + "sha256:d78485c4d21133d6b2fbb504cd544ca655e50b6eb551d2995b3aa6035928adda", + "sha256:d851247239548aa357c4a6840fb67adc2d570ce7cb56988d036a723d26b48bff", + "sha256:daca456d5b9f52e088e06a127dec182b3638a775684fb2260f25d664351cf1ae", + "sha256:dc346965847aef00013fa2364f41a64f068cd096dcc7778fc306ca3735f0eedf", + "sha256:dd2d2650faf54f9a0f5bd0db8409f79609319725f8f08af6507a0609deadcad4", + "sha256:e58e2b0c791041e4bccafa5bd7650623ba1592b8fe62ae0a276b7d0ecb314b6c", + "sha256:e708fc8f709a0fe1d1876489345f2e443d47f3976d33455e2e1e937f972f8677", + "sha256:ed9a3d98adafa0805ccbaaff5d2996a2b5795381285d8437a4a5d248dbd12b4a", + "sha256:edda1f7cff4806a4fa39e0e8ccd75f38d1d340fa5fc52d8582ade87aca247d92", + "sha256:f02c1c7505af18d426d355ac9872bd5c916b27f7b0fe224749930662bea47a50", + "sha256:f30d1618672a55e8c6669281ba264464b3ab563158e40d89e8c8b3faa0febebd", + "sha256:fe0228501ec616779a0b9c4299e837877783e18df294dd690b9ab0eed3d8aaab" + ], + "index": "pypi", + "version": "==2.5.2" + } + }, + "develop": {} +} diff --git a/maxman.py b/maxman.py new file mode 100644 index 0000000..694dace --- /dev/null +++ b/maxman.py @@ -0,0 +1,237 @@ +import abc +import dataclasses +import math +import random +import sys +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 random.random() * 100 < 10: + target = None + + if target is None: + if targets := [e for e in self.enemies or [] if e.size < self.man.size]: + 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() +ai = AI() +maxman = Man(pygame.Vector2(sw / 4, sh / 4), name="Maxman") +blackman = Man( + pygame.Vector2(sw * 0.75, sh * 0.75), + name="Blackman", + color="black", + key_up=pygame.K_i, + key_down=pygame.K_k, + key_left=pygame.K_j, + key_right=pygame.K_l, +) +players = [maxman, blackman] +food = Food.new() + +if any([arg in ["--ai", "-a"] for arg in sys.argv]): + ai.hook(blackman) + +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): + 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: + 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()