i151-arena-sdk 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- i151_arena_sdk-0.2.0/PKG-INFO +93 -0
- i151_arena_sdk-0.2.0/README.md +81 -0
- i151_arena_sdk-0.2.0/arena_sdk/__init__.py +27 -0
- i151_arena_sdk-0.2.0/arena_sdk/cli.py +150 -0
- i151_arena_sdk-0.2.0/arena_sdk/features.py +56 -0
- i151_arena_sdk-0.2.0/arena_sdk/init_project.py +47 -0
- i151_arena_sdk-0.2.0/arena_sdk/loader.py +179 -0
- i151_arena_sdk-0.2.0/arena_sdk/opponents.py +132 -0
- i151_arena_sdk-0.2.0/arena_sdk/public_examples/basic_bot/__pycache__/bot.cpython-314.pyc +0 -0
- i151_arena_sdk-0.2.0/arena_sdk/public_examples/basic_bot/bot.py +51 -0
- i151_arena_sdk-0.2.0/arena_sdk/public_examples/starter_bot/__pycache__/bot.cpython-314.pyc +0 -0
- i151_arena_sdk-0.2.0/arena_sdk/public_examples/starter_bot/bot.py +7 -0
- i151_arena_sdk-0.2.0/arena_sdk/templates/README.md +18 -0
- i151_arena_sdk-0.2.0/arena_sdk/templates/bot.py +8 -0
- i151_arena_sdk-0.2.0/arena_sdk/templates/gitignore +9 -0
- i151_arena_sdk-0.2.0/arena_sdk/templates/requirements.txt +1 -0
- i151_arena_sdk-0.2.0/i151_arena_sdk.egg-info/PKG-INFO +93 -0
- i151_arena_sdk-0.2.0/i151_arena_sdk.egg-info/SOURCES.txt +32 -0
- i151_arena_sdk-0.2.0/i151_arena_sdk.egg-info/dependency_links.txt +1 -0
- i151_arena_sdk-0.2.0/i151_arena_sdk.egg-info/entry_points.txt +2 -0
- i151_arena_sdk-0.2.0/i151_arena_sdk.egg-info/requires.txt +7 -0
- i151_arena_sdk-0.2.0/i151_arena_sdk.egg-info/top_level.txt +1 -0
- i151_arena_sdk-0.2.0/pyproject.toml +30 -0
- i151_arena_sdk-0.2.0/setup.cfg +4 -0
- i151_arena_sdk-0.2.0/tests/test_inference.py +747 -0
- i151_arena_sdk-0.2.0/tests/test_init.py +55 -0
- i151_arena_sdk-0.2.0/tests/test_loader.py +72 -0
- i151_arena_sdk-0.2.0/tests/test_mcts_alphazero.py +213 -0
- i151_arena_sdk-0.2.0/tests/test_mcts_bot.py +127 -0
- i151_arena_sdk-0.2.0/tests/test_mcts_train.py +327 -0
- i151_arena_sdk-0.2.0/tests/test_minmax_search.py +149 -0
- i151_arena_sdk-0.2.0/tests/test_ppo_bot.py +221 -0
- i151_arena_sdk-0.2.0/tests/test_rl_policy.py +1111 -0
- i151_arena_sdk-0.2.0/tests/test_sdk.py +134 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: i151-arena-sdk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: SDK étudiant pour la compétition IA i151
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: i151-engine>=0.1.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
10
|
+
Provides-Extra: ml
|
|
11
|
+
Requires-Dist: numpy>=1.26; extra == "ml"
|
|
12
|
+
|
|
13
|
+
# i151-arena-sdk — SDK étudiant
|
|
14
|
+
|
|
15
|
+
Package installable pour développer et tester un bot i151 en local, **sans cloner le dépôt complet**.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
21
|
+
pip install i151-arena-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
En développement (monorepo) :
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd arena/engine && pip install -e ".[dev]"
|
|
28
|
+
cd ../sdk && pip install -e ".[dev]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Démarrage rapide
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
arena init mon-bot
|
|
35
|
+
cd mon-bot
|
|
36
|
+
pip install -r requirements.txt
|
|
37
|
+
arena test bot.py
|
|
38
|
+
arena match bot.py --vs random --seed 42
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Exemples publics (inclus dans le package)
|
|
42
|
+
|
|
43
|
+
| Alias CLI | Description |
|
|
44
|
+
|-----------|-------------|
|
|
45
|
+
| `starter` | Première action légale |
|
|
46
|
+
| `basic` | Heuristique gloutonne |
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
arena test starter
|
|
50
|
+
arena match basic --vs random --seed 42
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Les bots avancés (MinMax, MCTS, RL, PPO) restent dans le monorepo pour le développement interne et ne sont **pas** publiés sur PyPI.
|
|
54
|
+
|
|
55
|
+
## Interface bot
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from arena_sdk import PlayerView, Action
|
|
59
|
+
|
|
60
|
+
def decide(view: PlayerView, legal_actions: list[Action], time_left_ms: int) -> Action:
|
|
61
|
+
return legal_actions[0]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## CLI
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
arena init mon-bot # créer un projet
|
|
68
|
+
arena test bot.py # smoke test vs random
|
|
69
|
+
arena match bot.py --vs random # match complet
|
|
70
|
+
arena match bot.py --vs basic # affronter l'exemple glouton
|
|
71
|
+
arena match bot.py --replay-out replay.json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## ML (local)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from arena_sdk.features import encode_view, action_from_index
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Voir [`../docs/TUTORIAL.md`](../docs/TUTORIAL.md) pour le guide complet.
|
|
81
|
+
|
|
82
|
+
## Dépendance
|
|
83
|
+
|
|
84
|
+
Ce package réexporte `PlayerView`, `Action` et `play_match` depuis `i151-engine` (installé automatiquement).
|
|
85
|
+
|
|
86
|
+
## Publication PyPI
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cd arena/engine && python -m build && twine upload dist/*
|
|
90
|
+
cd ../sdk && python -m build && twine upload dist/*
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Publier `i151-engine` avant `i151-arena-sdk`.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# i151-arena-sdk — SDK étudiant
|
|
2
|
+
|
|
3
|
+
Package installable pour développer et tester un bot i151 en local, **sans cloner le dépôt complet**.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
9
|
+
pip install i151-arena-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
En développement (monorepo) :
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd arena/engine && pip install -e ".[dev]"
|
|
16
|
+
cd ../sdk && pip install -e ".[dev]"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Démarrage rapide
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
arena init mon-bot
|
|
23
|
+
cd mon-bot
|
|
24
|
+
pip install -r requirements.txt
|
|
25
|
+
arena test bot.py
|
|
26
|
+
arena match bot.py --vs random --seed 42
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Exemples publics (inclus dans le package)
|
|
30
|
+
|
|
31
|
+
| Alias CLI | Description |
|
|
32
|
+
|-----------|-------------|
|
|
33
|
+
| `starter` | Première action légale |
|
|
34
|
+
| `basic` | Heuristique gloutonne |
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
arena test starter
|
|
38
|
+
arena match basic --vs random --seed 42
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Les bots avancés (MinMax, MCTS, RL, PPO) restent dans le monorepo pour le développement interne et ne sont **pas** publiés sur PyPI.
|
|
42
|
+
|
|
43
|
+
## Interface bot
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from arena_sdk import PlayerView, Action
|
|
47
|
+
|
|
48
|
+
def decide(view: PlayerView, legal_actions: list[Action], time_left_ms: int) -> Action:
|
|
49
|
+
return legal_actions[0]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## CLI
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
arena init mon-bot # créer un projet
|
|
56
|
+
arena test bot.py # smoke test vs random
|
|
57
|
+
arena match bot.py --vs random # match complet
|
|
58
|
+
arena match bot.py --vs basic # affronter l'exemple glouton
|
|
59
|
+
arena match bot.py --replay-out replay.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## ML (local)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from arena_sdk.features import encode_view, action_from_index
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Voir [`../docs/TUTORIAL.md`](../docs/TUTORIAL.md) pour le guide complet.
|
|
69
|
+
|
|
70
|
+
## Dépendance
|
|
71
|
+
|
|
72
|
+
Ce package réexporte `PlayerView`, `Action` et `play_match` depuis `i151-engine` (installé automatiquement).
|
|
73
|
+
|
|
74
|
+
## Publication PyPI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cd arena/engine && python -m build && twine upload dist/*
|
|
78
|
+
cd ../sdk && python -m build && twine upload dist/*
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Publier `i151-engine` avant `i151-arena-sdk`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""SDK étudiant — façade sur i151_engine."""
|
|
2
|
+
|
|
3
|
+
from i151_engine.models.action import Action, ActionType
|
|
4
|
+
from i151_engine.runner.config import MatchConfig
|
|
5
|
+
from i151_engine.runner.match_runner import MatchResult, play_match
|
|
6
|
+
from i151_engine.view.player_view import (
|
|
7
|
+
MatchContext,
|
|
8
|
+
OpponentState,
|
|
9
|
+
PlayerView,
|
|
10
|
+
TableState,
|
|
11
|
+
TurnState,
|
|
12
|
+
YouState,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Action",
|
|
17
|
+
"ActionType",
|
|
18
|
+
"MatchConfig",
|
|
19
|
+
"MatchContext",
|
|
20
|
+
"MatchResult",
|
|
21
|
+
"OpponentState",
|
|
22
|
+
"PlayerView",
|
|
23
|
+
"TableState",
|
|
24
|
+
"TurnState",
|
|
25
|
+
"YouState",
|
|
26
|
+
"play_match",
|
|
27
|
+
]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from i151_engine.runner.config import MatchConfig
|
|
9
|
+
from i151_engine.runner.match_runner import play_bots_match, play_match
|
|
10
|
+
|
|
11
|
+
from arena_sdk.init_project import InitProjectError, init_project
|
|
12
|
+
from arena_sdk.loader import load_submission
|
|
13
|
+
from arena_sdk.opponents import (
|
|
14
|
+
list_opponent_aliases,
|
|
15
|
+
resolve_opponent_arg,
|
|
16
|
+
resolve_submission_path,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: list[str] | None = None) -> int:
|
|
21
|
+
parser = argparse.ArgumentParser(prog="arena", description="CLI locale compétition i151")
|
|
22
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
23
|
+
|
|
24
|
+
match_parser = sub.add_parser("match", help="Lancer un match bot vs adversaire")
|
|
25
|
+
_bot_help = f"Bot : {list_opponent_aliases()} ou chemin/bot.py"
|
|
26
|
+
match_parser.add_argument("bot", help=_bot_help)
|
|
27
|
+
match_parser.add_argument(
|
|
28
|
+
"--vs",
|
|
29
|
+
dest="opponent_id",
|
|
30
|
+
default="builtin:random",
|
|
31
|
+
help=f"Adversaire : {list_opponent_aliases()} (défaut: builtin:random)",
|
|
32
|
+
)
|
|
33
|
+
match_parser.add_argument("--seed", type=int, default=42)
|
|
34
|
+
match_parser.add_argument("--target-score", type=int, default=151)
|
|
35
|
+
match_parser.add_argument(
|
|
36
|
+
"--cards-per-player",
|
|
37
|
+
type=int,
|
|
38
|
+
default=8,
|
|
39
|
+
help="Nombre de cartes distribuées à chaque joueur en début de manche (défaut 8)",
|
|
40
|
+
)
|
|
41
|
+
match_parser.add_argument("--json", action="store_true", help="Sortie JSON (résultat + replay)")
|
|
42
|
+
match_parser.add_argument("--replay-out", type=Path, help="Écrire le replay JSON dans un fichier")
|
|
43
|
+
|
|
44
|
+
test_parser = sub.add_parser("test", help="Smoke test : bot vs builtin:random")
|
|
45
|
+
test_parser.add_argument("bot", help=_bot_help)
|
|
46
|
+
test_parser.add_argument("--seed", type=int, default=42)
|
|
47
|
+
|
|
48
|
+
init_parser = sub.add_parser("init", help="Créer un nouveau projet bot (bot.py + requirements.txt)")
|
|
49
|
+
init_parser.add_argument(
|
|
50
|
+
"directory",
|
|
51
|
+
nargs="?",
|
|
52
|
+
default="mon-bot",
|
|
53
|
+
help="Dossier cible (défaut: mon-bot)",
|
|
54
|
+
)
|
|
55
|
+
init_parser.add_argument(
|
|
56
|
+
"--force",
|
|
57
|
+
action="store_true",
|
|
58
|
+
help="Écraser les fichiers template existants",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
args = parser.parse_args(argv)
|
|
62
|
+
|
|
63
|
+
if args.command == "match":
|
|
64
|
+
return _cmd_match(args)
|
|
65
|
+
if args.command == "test":
|
|
66
|
+
return _cmd_test(args)
|
|
67
|
+
if args.command == "init":
|
|
68
|
+
return _cmd_init(args)
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _cmd_match(args: argparse.Namespace) -> int:
|
|
73
|
+
bot_path = resolve_submission_path(args.bot)
|
|
74
|
+
bot = load_submission(bot_path)
|
|
75
|
+
challenger_dir = bot_path.parent
|
|
76
|
+
config = MatchConfig(
|
|
77
|
+
seed=args.seed,
|
|
78
|
+
target_score=args.target_score,
|
|
79
|
+
cards_per_player=args.cards_per_player,
|
|
80
|
+
)
|
|
81
|
+
opponent = resolve_opponent_arg(args.opponent_id)
|
|
82
|
+
result = play_bots_match(
|
|
83
|
+
bot,
|
|
84
|
+
opponent.bot,
|
|
85
|
+
config,
|
|
86
|
+
challenger_name=challenger_dir.name,
|
|
87
|
+
opponent_name=opponent.name,
|
|
88
|
+
opponent_id=opponent.opponent_id,
|
|
89
|
+
challenger_dir=challenger_dir,
|
|
90
|
+
opponent_dir=opponent.submission_dir,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if args.replay_out and result.replay is not None:
|
|
94
|
+
args.replay_out.write_text(result.replay.to_json(indent=2), encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
if args.json:
|
|
97
|
+
payload = {
|
|
98
|
+
"winner_id": result.winner_id,
|
|
99
|
+
"opponent_id": result.opponent_id,
|
|
100
|
+
"reason": result.reason,
|
|
101
|
+
"final_scores": result.final_scores,
|
|
102
|
+
"rounds_played": result.rounds_played,
|
|
103
|
+
"steps_played": result.steps_played,
|
|
104
|
+
"replay": result.replay.to_dict() if result.replay else None,
|
|
105
|
+
}
|
|
106
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
107
|
+
else:
|
|
108
|
+
print(f"Winner: {result.winner_id} ({result.reason})")
|
|
109
|
+
print(f"Scores: {result.final_scores}")
|
|
110
|
+
print(f"Rounds: {result.rounds_played}, steps: {result.steps_played}")
|
|
111
|
+
|
|
112
|
+
return 0 if result.reason == "normal" else 2
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _cmd_init(args: argparse.Namespace) -> int:
|
|
116
|
+
try:
|
|
117
|
+
created = init_project(Path(args.directory), force=args.force)
|
|
118
|
+
except InitProjectError as exc:
|
|
119
|
+
print(str(exc), file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
dest = Path(args.directory).resolve()
|
|
122
|
+
print(f"Projet créé dans {dest}")
|
|
123
|
+
for path in created:
|
|
124
|
+
print(f" + {path.name}")
|
|
125
|
+
print()
|
|
126
|
+
print("Prochaines étapes :")
|
|
127
|
+
print(f" cd {dest}")
|
|
128
|
+
print(" python3 -m venv .venv && source .venv/bin/activate")
|
|
129
|
+
print(" pip install -r requirements.txt")
|
|
130
|
+
print(" arena test bot.py")
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _cmd_test(args: argparse.Namespace) -> int:
|
|
135
|
+
bot = load_submission(resolve_submission_path(args.bot))
|
|
136
|
+
result = play_match(
|
|
137
|
+
bot,
|
|
138
|
+
"builtin:random",
|
|
139
|
+
MatchConfig(seed=args.seed, target_score=151, max_rounds=40),
|
|
140
|
+
challenger_name="test",
|
|
141
|
+
)
|
|
142
|
+
if result.reason != "normal":
|
|
143
|
+
print(f"Échec smoke test : {result.reason}", file=sys.stderr)
|
|
144
|
+
return 1
|
|
145
|
+
print("OK — match terminé sans forfait")
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from i151_engine.models.action import Action
|
|
4
|
+
from i151_engine.view.player_view import PlayerView
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def encode_view(view: PlayerView, legal_actions: list[Action]) -> list[float]:
|
|
8
|
+
"""Encodage de référence PlayerView → vecteur (usage ML local).
|
|
9
|
+
|
|
10
|
+
Schéma minimal v0 : scores, tailles de mains, état table, one-hot actions légales.
|
|
11
|
+
"""
|
|
12
|
+
features: list[float] = [
|
|
13
|
+
float(view.you.total_score),
|
|
14
|
+
float(view.you.hand_size),
|
|
15
|
+
float(view.you.hand_points),
|
|
16
|
+
float(view.you.consecutive_round_wins),
|
|
17
|
+
1.0 if view.you.is_chaining else 0.0,
|
|
18
|
+
float(view.table.bank_size),
|
|
19
|
+
float(view.table.played_pile_size),
|
|
20
|
+
1.0 if view.table.table_empty else 0.0,
|
|
21
|
+
1.0 if view.table.ace_effect_active else 0.0,
|
|
22
|
+
1.0 if view.table.must_choose_suit else 0.0,
|
|
23
|
+
float(view.match.round_number),
|
|
24
|
+
float(view.turn.step_index),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
if view.opponents:
|
|
28
|
+
opp = view.opponents[0]
|
|
29
|
+
features.extend(
|
|
30
|
+
[
|
|
31
|
+
float(opp.total_score),
|
|
32
|
+
float(opp.hand_size),
|
|
33
|
+
float(opp.consecutive_round_wins),
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
features.extend([0.0, 0.0, 0.0])
|
|
38
|
+
|
|
39
|
+
for action in legal_actions:
|
|
40
|
+
features.append(float(hash((action.type.value, action.card, action.chosen_suit)) % 1000) / 1000.0)
|
|
41
|
+
|
|
42
|
+
return features
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def action_index(legal_actions: list[Action], action: Action) -> int:
|
|
46
|
+
try:
|
|
47
|
+
return legal_actions.index(action)
|
|
48
|
+
except ValueError:
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def action_from_index(legal_actions: list[Action], index: int) -> Action:
|
|
53
|
+
if not legal_actions:
|
|
54
|
+
raise ValueError("legal_actions vide")
|
|
55
|
+
clamped = max(0, min(index, len(legal_actions) - 1))
|
|
56
|
+
return legal_actions[clamped]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Génération d'un projet bot étudiant via `arena init`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.resources
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_TEMPLATE_FILES = {
|
|
9
|
+
"bot.py": "bot.py",
|
|
10
|
+
"requirements.txt": "requirements.txt",
|
|
11
|
+
"README.md": "README.md",
|
|
12
|
+
".gitignore": "gitignore",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InitProjectError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _template_text(filename: str) -> str:
|
|
21
|
+
package = importlib.resources.files("arena_sdk").joinpath("templates", filename)
|
|
22
|
+
return package.read_text(encoding="utf-8")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def init_project(target: Path, *, force: bool = False) -> list[Path]:
|
|
26
|
+
"""Crée un dossier de projet bot avec bot.py, requirements.txt, README.md."""
|
|
27
|
+
dest = target.resolve()
|
|
28
|
+
if dest.exists():
|
|
29
|
+
if not dest.is_dir():
|
|
30
|
+
raise InitProjectError(f"La cible existe et n'est pas un dossier : {dest}")
|
|
31
|
+
conflicts = [name for name in _TEMPLATE_FILES if (dest / name).exists()]
|
|
32
|
+
if conflicts and not force:
|
|
33
|
+
raise InitProjectError(
|
|
34
|
+
f"Fichiers déjà présents dans {dest} : {', '.join(conflicts)}. "
|
|
35
|
+
"Utilisez un autre dossier ou --force."
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
dest.mkdir(parents=True)
|
|
39
|
+
|
|
40
|
+
created: list[Path] = []
|
|
41
|
+
for dest_name, template_name in _TEMPLATE_FILES.items():
|
|
42
|
+
out = dest / dest_name
|
|
43
|
+
if out.exists() and not force:
|
|
44
|
+
continue
|
|
45
|
+
out.write_text(_template_text(template_name), encoding="utf-8")
|
|
46
|
+
created.append(out)
|
|
47
|
+
return created
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import importlib.util
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
from i151_engine.bots.protocol import Bot
|
|
10
|
+
from i151_engine.models.action import Action
|
|
11
|
+
from i151_engine.view.player_view import PlayerView
|
|
12
|
+
|
|
13
|
+
_KNOWN_PACKAGES = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"arena_sdk",
|
|
16
|
+
"i151_engine",
|
|
17
|
+
"numpy",
|
|
18
|
+
"sklearn",
|
|
19
|
+
"joblib",
|
|
20
|
+
"onnxruntime",
|
|
21
|
+
"torch",
|
|
22
|
+
"pandas",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SubmissionBot:
|
|
28
|
+
"""Adapte un module bot.py étudiant (setup / decide / teardown) au protocole Bot."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, module: ModuleType, submission_dir: Path) -> None:
|
|
31
|
+
self._module = module
|
|
32
|
+
self._submission_dir = submission_dir
|
|
33
|
+
|
|
34
|
+
def setup(self, submission_dir: Path) -> None:
|
|
35
|
+
del submission_dir
|
|
36
|
+
setup = getattr(self._module, "setup", None)
|
|
37
|
+
if callable(setup):
|
|
38
|
+
setup(self._submission_dir)
|
|
39
|
+
|
|
40
|
+
def decide(
|
|
41
|
+
self,
|
|
42
|
+
view: PlayerView,
|
|
43
|
+
legal_actions: list[Action],
|
|
44
|
+
time_left_ms: int,
|
|
45
|
+
) -> Action:
|
|
46
|
+
decide = self._module.decide
|
|
47
|
+
return decide(view, legal_actions, time_left_ms)
|
|
48
|
+
|
|
49
|
+
def teardown(self) -> None:
|
|
50
|
+
teardown = getattr(self._module, "teardown", None)
|
|
51
|
+
if callable(teardown):
|
|
52
|
+
teardown()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _local_import_roots(source: str) -> set[str]:
|
|
56
|
+
try:
|
|
57
|
+
tree = ast.parse(source)
|
|
58
|
+
except SyntaxError as exc:
|
|
59
|
+
raise ImportError(f"Syntaxe bot.py invalide : {exc}") from exc
|
|
60
|
+
|
|
61
|
+
allowed = _KNOWN_PACKAGES | sys.stdlib_module_names
|
|
62
|
+
local: set[str] = set()
|
|
63
|
+
for node in ast.walk(tree):
|
|
64
|
+
if isinstance(node, ast.Import):
|
|
65
|
+
for alias in node.names:
|
|
66
|
+
root = alias.name.split(".")[0]
|
|
67
|
+
if root not in allowed:
|
|
68
|
+
local.add(root)
|
|
69
|
+
elif isinstance(node, ast.ImportFrom):
|
|
70
|
+
if node.level and node.level > 0:
|
|
71
|
+
continue
|
|
72
|
+
if node.module:
|
|
73
|
+
root = node.module.split(".")[0]
|
|
74
|
+
if root not in allowed:
|
|
75
|
+
local.add(root)
|
|
76
|
+
return local
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _collect_local_module_closure(
|
|
80
|
+
submission_dir: Path,
|
|
81
|
+
extract_root: Path | None,
|
|
82
|
+
seeds: set[str],
|
|
83
|
+
) -> list[str]:
|
|
84
|
+
"""Modules locaux transitifs, ordre de chargement (dépendances d'abord)."""
|
|
85
|
+
order: list[str] = []
|
|
86
|
+
seen: set[str] = set()
|
|
87
|
+
|
|
88
|
+
def visit(name: str) -> None:
|
|
89
|
+
if name in seen:
|
|
90
|
+
return
|
|
91
|
+
seen.add(name)
|
|
92
|
+
py_file = _find_local_module(submission_dir, extract_root, name)
|
|
93
|
+
if py_file is not None:
|
|
94
|
+
source = py_file.read_text(encoding="utf-8")
|
|
95
|
+
for dep in sorted(_local_import_roots(source)):
|
|
96
|
+
visit(dep)
|
|
97
|
+
order.append(name)
|
|
98
|
+
|
|
99
|
+
for seed in sorted(seeds):
|
|
100
|
+
visit(seed)
|
|
101
|
+
return order
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _find_local_module(
|
|
105
|
+
submission_dir: Path,
|
|
106
|
+
extract_root: Path | None,
|
|
107
|
+
module: str,
|
|
108
|
+
) -> Path | None:
|
|
109
|
+
sibling = submission_dir / f"{module}.py"
|
|
110
|
+
if sibling.is_file():
|
|
111
|
+
return sibling
|
|
112
|
+
if extract_root is not None:
|
|
113
|
+
matches = sorted(extract_root.rglob(f"{module}.py"))
|
|
114
|
+
if matches:
|
|
115
|
+
return matches[0]
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _preload_local_modules(
|
|
120
|
+
*,
|
|
121
|
+
submission_dir: Path,
|
|
122
|
+
extract_root: Path | None,
|
|
123
|
+
module_names: list[str],
|
|
124
|
+
) -> None:
|
|
125
|
+
for mod_name in module_names:
|
|
126
|
+
py_file = _find_local_module(submission_dir, extract_root, mod_name)
|
|
127
|
+
if py_file is None:
|
|
128
|
+
raise ModuleNotFoundError(
|
|
129
|
+
f"No module named '{mod_name}' — ajoutez {mod_name}.py "
|
|
130
|
+
f"dans le même dossier que bot.py"
|
|
131
|
+
)
|
|
132
|
+
if mod_name in sys.modules:
|
|
133
|
+
continue
|
|
134
|
+
spec = importlib.util.spec_from_file_location(mod_name, py_file)
|
|
135
|
+
if spec is None or spec.loader is None:
|
|
136
|
+
raise ImportError(f"Impossible de charger {py_file}")
|
|
137
|
+
module = importlib.util.module_from_spec(spec)
|
|
138
|
+
sys.modules[mod_name] = module
|
|
139
|
+
spec.loader.exec_module(module)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def load_submission(bot_path: Path, *, extract_root: Path | None = None) -> Bot:
|
|
143
|
+
"""Charge bot.py depuis un fichier ou un dossier de soumission."""
|
|
144
|
+
path = bot_path.resolve()
|
|
145
|
+
if path.is_dir():
|
|
146
|
+
submission_dir = path
|
|
147
|
+
module_path = path / "bot.py"
|
|
148
|
+
else:
|
|
149
|
+
submission_dir = path.parent
|
|
150
|
+
module_path = path
|
|
151
|
+
|
|
152
|
+
if not module_path.is_file():
|
|
153
|
+
raise FileNotFoundError(f"bot.py introuvable : {module_path}")
|
|
154
|
+
|
|
155
|
+
resolved_extract = extract_root.resolve() if extract_root is not None else None
|
|
156
|
+
bot_source = module_path.read_text(encoding="utf-8")
|
|
157
|
+
local_modules = _collect_local_module_closure(
|
|
158
|
+
submission_dir,
|
|
159
|
+
resolved_extract,
|
|
160
|
+
_local_import_roots(bot_source),
|
|
161
|
+
)
|
|
162
|
+
_preload_local_modules(
|
|
163
|
+
submission_dir=submission_dir,
|
|
164
|
+
extract_root=resolved_extract,
|
|
165
|
+
module_names=local_modules,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
spec = importlib.util.spec_from_file_location("student_bot", module_path)
|
|
169
|
+
if spec is None or spec.loader is None:
|
|
170
|
+
raise ImportError(f"Impossible de charger {module_path}")
|
|
171
|
+
|
|
172
|
+
module = importlib.util.module_from_spec(spec)
|
|
173
|
+
sys.modules["student_bot"] = module
|
|
174
|
+
spec.loader.exec_module(module)
|
|
175
|
+
|
|
176
|
+
if not callable(getattr(module, "decide", None)):
|
|
177
|
+
raise AttributeError("bot.py doit définir une fonction decide(view, legal_actions, time_left_ms)")
|
|
178
|
+
|
|
179
|
+
return SubmissionBot(module, submission_dir)
|