planetwars.dev/examples/python/planetwars.py
2023-11-20 16:34:47 +01:00

285 lines
9.7 KiB
Python

from __future__ import annotations
import sys, json, math
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
"""
============================================
______ _ _______ _____
|___ / | | | | ___ \_ _|
/ / ___ _ _ ___ | | | | |_/ / | |
/ / / _ \ | | / __| | |/\| | __/ | |
./ /__| __/ |_| \__ \ \ /\ / | _| |_
\_____/\___|\__,_|___/ \/ \/\_| \___/
============================================
Deze file bevatten een aantal klassen die het makkelijker maken om te werken met de input
die je krijgt van de game Planetwars.
De belangrijkste klasse hierin is PlanetWars.
Deze laat je toe om makkelijk de staat van het spel bij te houden.
De bedoeling van de klasse is dat je hem eenmaal aanmaakt en dan
bij elke beurt de update() functie aanroept met de data die je krijgt van het spel.
Daarna heb je alle data in de variabelen
- my_planets
- neutral_planets
- ...
- enemy_expeditions
Die bestaan uit lijsten de klassen Planet en Expedition.
De bedoeling van die klassen is om makkelijk met de data overweg te kunnen
en NIET om zelf een instance van aan te maken (zoals aangegeven door @dataclass)
Je kan dan de functie add_move() gebruiken om een bepaalde zet die je wilt doen toe te voegen.
Voeg er zoveel toe als je wilt en gebruik dan de functie move() om ze effectief door te geven.
Daarna kan je weer wachten op nieuwe data van het spel.
De klasse heeft nog twee andere variabelen.
- _queued_moves
- current_turn
Het is niet de bedoeling om van buiten de klasse aan _queued_moves te komen (aangegeven door de leidende _).
Wil je toch extra functionaliteit toevoegen om bijvoorbeeld een bepaalde move te verwijderen
voeg dat dan toe als een functie binnenin de klasse.
Tenslotte heb je nog de variable current_turn.
Die wordt momenteel niet gebruikt en houd alleen bij aan welke turn we zitten.
Je kan de file example.py bekijken met een voorbeeld implementatie.
"""
def log(message: str):
"""Logs een gegeven bericht met een tijdstip ervoor."""
timestamp = datetime.now().strftime("%H:%M:%S")
print(f"[{timestamp}] {message}", file=sys.stderr)
@dataclass
class Planet():
"""
Stelt een planeet voor.
Niet de bedoeling dat je deze klasse zelf aanmaakt.
"""
owner: Optional[int]
ship_count: int
name: str
x: float
y: float
@staticmethod
def _from_input(planet: dict):
"""
Maakt een Planet object gegeven de input die je krijgt van de game.
Moet je niet zelf gebruiken.
"""
return Planet(planet["owner"], planet["ship_count"], planet["name"], planet["x"], planet["y"])
@staticmethod
def distance_between(p1: Planet, p2: Planet):
"""Geeft het aantal beurten terug die nodig zijn om een bepaalde planeet te bereiken."""
return math.ceil(math.dist([p1.x, p1.y], [p2.x, p2.y]))
@dataclass
class Expedition():
"""
Stelt een expeditie voor.
Niet de bedoeling dat je deze klasse zelf aanmaakt.
"""
id: int
ship_count: int
origin: Planet
destination: Planet
owner: int
turns_remaining: int
@staticmethod
def _from_input(exp: dict, planets: list[Planet]):
"""
Maakt een Expedition object gegeven de input die je krijgt van de game.
Moet je niet zelf gebruiken.
"""
origin = next((planet for planet in planets if planet.name == exp["origin"]), None)
destination = next((planet for planet in planets if planet.name == exp["destination"]), None)
if None in (origin, destination):
log("Error: No origin or destination planet found")
log(planets)
raise KeyError("Origin or destination planet not found")
return Expedition(exp["id"], exp["ship_count"], origin, destination, exp["owner"], exp["turns_remaining"])
@dataclass
class Move():
"""
Stelt een zet voor.
Niet de bedoeling dat je deze klasse zelf aanmaakt.
"""
origin: Planet
destination: Planet
ship_count: int
def get_dict(self):
"""Maak een dictionary van de gegevens zodat het kan gezien worden als een zet."""
return {
"origin": self.origin.name,
"destination": self.destination.name,
"ship_count": self.ship_count
}
class PlanetWars():
"""Houdt de staat van het spel bij."""
my_planets: list[Planet] = [] # Lijst van de planeten onder jouw controle.
neutral_planets: list[Planet] = [] # Lijst van de planeten die niemand heeft.
enemy_planets: list[Planet] = [] # Lijst van de planeten onder de controle van de tegenstander.
my_expeditions: list[Expedition] = [] # Alle huidige expedities die van jouw zijn.
enemy_expeditions: list[Expedition] = [] # Alle huidige expedities die van de tegenstander zijn.
_queued_moves: list[Move] = [] # Alle moves die je klaar hebt staan om uit te voeren. Kom hier niet rechtstreeks aan.
_moved = False # Houd bij of je al een move hebt gespeeld deze beurt.
current_turn = 0 # Aantal beurten die al gepasseerd zijn.
def update(self, input: str):
"""
Roep dit iedere beurt op met de input die je krijgt van het spel.
Gaat over de data en maakt er de juiste objecten van.
Args:
- input (str): De data die je krijgt van het spel. Zie example.py hoe je dit het best krijgt
Returns:
- bool: Geeft aan of opgegeven data klopt en of je iets kan doen.
"""
try:
state = json.loads(input)
except:
log("Error: Converting input to json")
log(input)
return False
try:
planets = [Planet._from_input(planet) for planet in state["planets"]]
except KeyError:
log("Error: Parsing planet input")
log(input)
return False
if len(planets) == 0:
log("Error: No planets")
log(input)
return False
self.my_planets = [planet for planet in planets if planet.owner == 1]
self.neutral_planets = [planet for planet in planets if planet.owner is None]
self.enemy_planets = [planet for planet in planets if planet.owner not in [None, 1]]
try:
self.my_expeditions = [Expedition._from_input(expedition, planets) for expedition in state["expeditions"] if expedition["owner"] == 1]
self.enemy_expeditions = [Expedition._from_input(expedition, planets) for expedition in state["expeditions"] if expedition["owner"] != 1]
except KeyError:
log("Error: Parsing expedition input")
log(input)
return False
self._queued_moves.clear()
self._moved = False
self.current_turn += 1
return True
def add_move(self, origin: Planet, destination: Planet, ships: int):
"""
Voeg een zet toe.
Je kan zoveel zetten toevoegen als je wilt en ze worden pas doorgegeven wanneer je move() aanroept.
Args:
- origin (Planet): De planeet van waar de schepen moeten komen.
- destination (Planet): De planeet waar de schepen moeten toekomen.
- ships (int): De hoeveelheid schepen die je wilt versturen
Returns:
- bool: Geeft weer indien het een geldige zet is. Let op er worden maar een paar basis checks gedaan hiervoor
"""
# Je kan maar 1 keer al je moves versturen per buurt.
# Je kan daarin wel meerdere moves zetten.
# Je kan dit doen door meerdere keren add_move op te roepen voor je move() oproept.
if self._moved:
return False
# Je kan alleen schepen sturen die komen van je eigen planeet
if origin.owner != 1:
return False
# Je kan geen schepen sturen naar dezelfde planeet als van waar ze komen
if origin.name == destination.name:
return False
# Je kan niet meer schepen versturen dan je er zijn op de oorsprong planeet
total_ships = ships + sum(move.ship_count for move in self._queued_moves if move.origin.name == origin.name)
if total_ships > origin.ship_count:
return False
self._queued_moves.append(Move(origin, destination, ships))
return True
def clear_moves(self):
"""Verwijder de moves die je al had toegevoegd"""
self._queued_moves.clear()
def move(self):
"""Voer alle zetten uit en ga naar de volgende beurt"""
# Je kan maar 1 keer al je moves versturen per buurt.
# Je kan daarin wel meerdere moves zetten.
# Je kan dit doen door meerdere keren add_move op te roepen voor je move() oproept.
if self._moved:
return
moves = [move.get_dict() for move in self._queued_moves]
self._moved = True
print(json.dumps({ "moves": moves }))
sys.stdout.flush()
"""
Weet je niet hoe je nu verder moet?
Kijk naar example.py hoe de klasse te implementeren.
Nog enkele tips hoe je je bot beter kan maken:
- Maak een nieuwe functie aan in de klasse PlanetWars `predict`.
Deze probeert te voorspellen hoeveel schepen van je tegenstander er op een bepaalde
planeet toekomen na x aantal beurten.
- Maak een onderscheid tussen een gevallen waarbij jouw tegenstander geen extra schepen meer
verstuurd naar een bepaalde planeet en al zijn schepen er naar verstuurd.
Welke planeten kan je met de minste effort makkelijk pakken?
- Indien je al meerdere planeten hebt probeer schepen van de achterliggende planeten naar
de frontline te sturen.
- ...
Wees creatief, de mogelijkheden zijn eindeloos :D
"""