Skip to content

Commit 5444357

Browse files
authored
Python Starter Bot (#5)
* Python bot init talks with server * Python talk with server up to started match * Python starter bot client done * - Fixed socket read buffer - Named tuple XY and BotInfo * - Revert back to dataclasses (faster) * - Socket - skip server round data to stay in sync * - README - bot comments - optimized Socket performance
1 parent d22da43 commit 5444357

File tree

14 files changed

+340
-2
lines changed

14 files changed

+340
-2
lines changed

build.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
##Структура проекта
1+
## Структура проекта
22

33
- `common` - модуль с общими классами, используемыми и на стороне сервера, и на стороне бота
44
- `server` - модуль с кодом сервера. Дорабатывается исключительно администраторами.
55
- `starter-bot` - модуль-заготовка для бота. Он же - простейший пример. Можно брать за основу для работы над ботом.
66

7-
##Сборка и запуск проекта
7+
## Сборка и запуск проекта
88

99
Сборка проекта осуществляется с помощью [Maven](https://ru.wikipedia.org/wiki/Apache_Maven).
1010
Шаги сборки:

starter-bot-python/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## 🐍 Python Starter bot
2+
3+
### Как использовать
4+
5+
[`main.py`](main.py) - настройки подключения к серверу
6+
7+
[`bot.py`](bot.py) - код бота и его параметры
8+
9+
Требования: **Python 3.10+**
10+
11+
---
12+
_[Артем **nGragas** Корников](https://t.me/Rush_iam) для [Croc Bot Battle](https://brainz.croc.ru/hello-work)_

starter-bot-python/bot.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from client.bot_base import BotBase
2+
from client.message.extra_types import Mode
3+
from client.message.messages import MatchStarted, Update
4+
5+
# Параметры регистрации бота на сервере
6+
bot_name = 'SuperStarter'
7+
bot_secret = ''
8+
mode = Mode.FRIENDLY
9+
10+
11+
class Bot(BotBase):
12+
# Старт
13+
def on_match_start(self, match_info: MatchStarted):
14+
# Правила матча
15+
self.match_info = match_info
16+
self.id = match_info.your_id
17+
print(match_info)
18+
print(f'Матч стартовал! Бот <{self.name}> готов')
19+
20+
# Каждый ход
21+
def on_update(self, update: Update) -> tuple[int, int]:
22+
# Данные раунда: что бот "видит"
23+
round_number = update.round
24+
coins = update.coin
25+
blocks = update.block
26+
my_bot = next((bot for bot in update.bot if bot.id == self.id), None)
27+
opponents = [bot for bot in update.bot if bot.id != self.id]
28+
29+
# Выбираем направление движения
30+
import random
31+
dx = random.choice([-1, 0, 1])
32+
dy = random.choice([-1, 0, 1])
33+
34+
return dx, dy # Отправляем ход серверу
35+
36+
# Конец матча
37+
def on_match_over(self) -> None:
38+
print('Матч окончен')

starter-bot-python/client/__init__.py

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from dataclasses import dataclass, field
2+
from typing import TypeVar
3+
4+
from client.message.extra_types import Mode
5+
from client.message.messages import MatchStarted, Update
6+
7+
8+
BotImpl = TypeVar('BotImpl', bound='BotBase')
9+
10+
11+
@dataclass
12+
class BotBase:
13+
name: str
14+
secret: str = None
15+
mode: Mode = Mode.FRIENDLY
16+
match_info: MatchStarted = field(init=False)
17+
id: int = field(init=False)
18+
19+
def on_match_start(self, match_info: MatchStarted):
20+
raise NotImplementedError
21+
22+
def on_update(self, update: Update) -> tuple[int, int]:
23+
raise NotImplementedError
24+
25+
def on_match_over(self) -> None:
26+
raise NotImplementedError
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from .bot_base import BotImpl
2+
from .client import HypernullClient
3+
from .message import messages
4+
5+
6+
class BotMatchRunner:
7+
def __init__(self, bot: BotImpl, client: HypernullClient):
8+
self.bot = bot
9+
self.client = client
10+
11+
def run(self) -> None:
12+
self.client.register(self.bot)
13+
14+
match_info: messages.MatchStarted = self.client.get()
15+
self.bot.on_match_start(match_info)
16+
17+
while update := self.client.get_update():
18+
dx, dy = self.bot.on_update(update)
19+
self.client.move(dx, dy)
20+
21+
self.bot.on_match_over()
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from .bot_base import BotImpl
2+
from .socket_session import SocketSession
3+
from .message import factory, messages, extra_types
4+
5+
6+
class HypernullClient:
7+
version: int = 1
8+
9+
def __init__(self, host: str = 'localhost', port: int = 2021):
10+
self.session = SocketSession(host, port)
11+
msg = self.get()
12+
if not isinstance(msg, messages.Hello):
13+
raise Exception(
14+
f'Wanted message {messages.Hello.__name__}, got: {type(msg)}'
15+
)
16+
17+
if msg.protocol_version != self.version:
18+
raise Exception(
19+
f'Client v{self.version}, but Server v{msg.protocol_version}'
20+
)
21+
22+
def get(self) -> factory.Message:
23+
data = self.session.read()
24+
return factory.MessageFactory.load(data)
25+
26+
def send(self, msg: messages.MessageBase) -> None:
27+
data = msg.dump()
28+
self.session.write(data)
29+
30+
def register(self, bot: BotImpl) -> None:
31+
register = messages.Register(
32+
bot_name=bot.name,
33+
bot_secret=bot.secret,
34+
mode=bot.mode,
35+
)
36+
self.send(register)
37+
38+
def get_update(self) -> messages.Update | None:
39+
update: messages.Update | messages.MatchOver = self.get()
40+
if isinstance(update, messages.MatchOver):
41+
return None
42+
return update
43+
44+
def move(self, dx: int, dy: int) -> None:
45+
move = messages.Move(
46+
offset=extra_types.XY(dx, dy)
47+
)
48+
self.send(move)

starter-bot-python/client/message/__init__.py

Whitespace-only changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from dataclasses import dataclass, asdict
2+
3+
4+
@dataclass
5+
class MessageBase:
6+
@classmethod
7+
def type(cls) -> str:
8+
return cls.__name__.lower()
9+
10+
def dump(self) -> str:
11+
command = self.type()
12+
params = '\n'.join(
13+
f'{k} {" ".join(map(str, v.values())) if isinstance(v, dict) else v}'
14+
for k, v in asdict(self).items() if v not in [None, '']
15+
)
16+
return f'{command}\n' \
17+
f'{params}\n' \
18+
f'end\n'
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from dataclasses import dataclass
2+
from enum import Enum
3+
4+
5+
class Mode(str, Enum):
6+
FRIENDLY = 'FRIENDLY'
7+
DEATHMATCH = 'DEATHMATCH'
8+
9+
10+
@dataclass
11+
class XY:
12+
x: int
13+
y: int
14+
15+
16+
@dataclass
17+
class BotInfo(XY):
18+
coins: int
19+
id: int

0 commit comments

Comments
 (0)