Skip to content

Commit

Permalink
Merge pull request #29 from ALLAN-DIP/add-baseline-lr-advisor
Browse files Browse the repository at this point in the history
Add baseline LR advisor
  • Loading branch information
gale2307 authored Feb 5, 2025
2 parents 58d5cb7 + ecb213f commit e095a92
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# Use the command `hadolint Dockerfile` to test
# Adding Hadolint to `pre-commit` is non-trivial, so the command must be run manually

FROM ghcr.io/allan-dip/chiron-utils:baseline-lr-model-2025-01-14 AS baseline-lr-model

FROM python:3.11.11-slim-bookworm AS base

WORKDIR /bot
Expand Down Expand Up @@ -34,3 +36,7 @@ RUN pip install --no-cache-dir --no-deps -e .[$TARGET]
ENTRYPOINT ["python", "-m", "chiron_utils.scripts.run_bot"]

LABEL org.opencontainers.image.source=https://github.com/ALLAN-DIP/chiron-utils

FROM base AS baseline-lr

COPY --from=baseline-lr-model lr_model/ lr_model/
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,8 @@ build:
--tag ghcr.io/allan-dip/chiron-utils:$(TAG) \
--target $(TARGET) \
.

.PHONY: build-baseline-lr
build-baseline-lr:
TARGET=baseline-lr \
make build
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ Both the bot and game running commands support a `--help` argument to list avail
- Messages are proposals to carry out a set of valid moves, which is also randomly selected. One such proposal is sent to each opponent.
- Due to the random nature of play, a game consisting entirely of `RandomProposerPlayer`s can last for a very long time. I (Alex) have observed multiple games lasting past 1950 without a clear winner.
- `RandomProposerPlayer` uses very few resources, so it is useful as stand-ins for other players.
- [`LrBot`](src/chiron_utils/bots/lr_bot.py) (`LrAdvisor` and `LrPlayer`):
- A logistic regression model is used to predict orders for each available unit, given current game state.
- To build the bot, run `make build-baseline-lr` to generate the OCI image to run with Docker
- When running the bot outside of a container, download the latest model file from [lr_models - Google Drive](https://drive.google.com/drive/folders/1FuG3qY51wRkR8RgEBVY49-loln06W-Ro). The filename includes the model release date in `YYYYMMDD` format).
- Edit the `MODEL_PATH` constant in `lr_bot.py` to point to the unzipped model folder.
- Code for model training can be found at <https://github.com/ALLAN-DIP/baseline-models>

## Contributing

Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,22 @@ dependencies = [
"daidepp @ git+https://git@github.com/SHADE-AI/daidepp.git@f84f7279fb4a31083ad8e4b5fcf6d1ed290bae0e",
# Pinned `diplomacy` to `main`
"diplomacy @ git+https://git@github.com/ALLAN-DIP/diplomacy.git@5733af848f3e5b35cd9ff8a1eba77bb40e09c946",
'importlib-metadata ; python_version < "3.10"',
"tornado",
]

[project.optional-dependencies]
# `all` extra allows an easy install of all optional dependencies
all = [
"chiron_utils[base]",
"chiron_utils[baseline-lr]",
"chiron_utils[dev]",
]
# Default extra to make OCI image builds easier
base = []
baseline-lr = [
"baseline-models @ git+https://git@github.com/ALLAN-DIP/baseline-models.git@2fd62f89963eb804f5f98ba0a1321656e28e594a",
]
dev = [
# Use older `mypy` version to keep code compatible with Python 3.7
"mypy<1.9",
Expand Down
6 changes: 6 additions & 0 deletions requirements-lock.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
astroid==3.3.8
baseline_models @ git+https://git@github.com/ALLAN-DIP/baseline-models.git@2fd62f89963eb804f5f98ba0a1321656e28e594a
bcrypt==4.2.1
certifi==2024.12.14
cfgv==3.4.0
Expand All @@ -14,10 +15,12 @@ identify==2.6.6
idna==3.10
iniconfig==2.0.0
isort==6.0.0
joblib==1.4.2
mccabe==0.7.0
mypy==1.8.0
mypy-extensions==1.0.0
nodeenv==1.9.1
numpy==1.26.4
packaging==24.2
parsimonious==0.9.0
platformdirs==4.3.6
Expand All @@ -30,8 +33,11 @@ pytz==2024.2
PyYAML==6.0.2
regex==2024.11.6
requests==2.32.3
scikit-learn==1.4.1.post1
scipy==1.15.1
setuptools==75.8.0
six==1.17.0
threadpoolctl==3.5.0
tomlkit==0.13.2
tornado==6.4.2
tqdm==4.67.1
Expand Down
24 changes: 24 additions & 0 deletions src/chiron_utils/bots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

from typing import List, Type

try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata

from chiron_utils.bots.baseline_bot import BaselineBot as BaselineBot
from chiron_utils.bots.random_proposer_bot import (
RandomProposerAdvisor as RandomProposerAdvisor,
Expand All @@ -12,6 +17,25 @@
RandomProposerAdvisor,
RandomProposerPlayer,
]
# Import bots only if their direct third-party dependencies are satisfied
# This unfortunately requires hardcoding the list of required modules,
# but there currently isn't a way to check if a given extra was used during installation.
importable_modules = set(importlib_metadata.packages_distributions())
if {"baseline_models"} < importable_modules:
from chiron_utils.bots.lr_bot import (
LrAdvisor as LrAdvisor,
LrPlayer as LrPlayer,
)

BOTS.extend(
[
LrAdvisor,
LrPlayer,
]
)
# Alphabetize list of classes
BOTS.sort(key=lambda t: t.__name__)

NAMES_TO_BOTS = {bot.__name__: bot for bot in BOTS}

DEFAULT_BOT_TYPE = RandomProposerPlayer # pylint: disable=invalid-name
92 changes: 92 additions & 0 deletions src/chiron_utils/bots/lr_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Bots that carry out orders generated by a logistic regressor model."""

from abc import ABC
from dataclasses import dataclass
from pathlib import Path
from typing import List, Sequence

from baseline_models.model_code.predict import predict
from diplomacy.utils import strings as diplomacy_strings
from diplomacy.utils.constants import SuggestionType

from chiron_utils.bots.baseline_bot import BaselineBot, BotType
from chiron_utils.utils import return_logger

logger = return_logger(__name__)

MODEL_PATH = Path() / "lr_model"


@dataclass
class LrBot(BaselineBot, ABC):
"""Baseline logistic regressor model.
`MODEL_PATH` should point to folder containing model .pkl files.
Each model corresponds to a (unit, location, phase) combination.
Unit types are 'A', 'F'.
Location types include all possible locations on the board, such as 'BRE', 'LON', 'STP_SC', etc.
Phase types are 'SM', 'FM', 'WA, 'SR', 'FR', 'CD'
"""

player_type = diplomacy_strings.NO_PRESS_BOT

def __post_init__(self) -> None:
"""Verify that model path exists when instantiated."""
super().__post_init__()
if not MODEL_PATH.is_dir():
raise NotADirectoryError(
f"Model directory {str(MODEL_PATH)!r} does not exist or is not a directory."
)

def get_orders(self) -> List[str]:
"""Get order predictions from model.
Returns:
List of predicted orders.
"""
state = self.game.get_state()
orders: List[str] = predict(MODEL_PATH, state, self.power_name)

logger.info("Orders to suggest: %s", orders)

return orders

async def gen_orders(self) -> List[str]:
"""Generate orders for a turn.
Returns:
List of orders to carry out.
"""
orders = self.get_orders()
if self.bot_type == BotType.ADVISOR:
await self.suggest_orders(orders)
elif self.bot_type == BotType.PLAYER:
await self.send_orders(orders, wait=True)
return orders

async def do_messaging_round(self, orders: Sequence[str]) -> List[str]:
"""Carry out one round of messaging.
Returns:
List of orders to carry out.
"""
return list(orders)


@dataclass
class LrAdvisor(LrBot):
"""Advisor form of `LrBot`."""

bot_type = BotType.ADVISOR
default_suggestion_type = SuggestionType.MOVE


@dataclass
class LrPlayer(LrBot):
"""Player form of `LrBot`."""

bot_type = BotType.PLAYER

0 comments on commit e095a92

Please sign in to comment.