frondori-engine 0.4.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.
- frondori_engine-0.4.0/LICENSE +21 -0
- frondori_engine-0.4.0/PKG-INFO +120 -0
- frondori_engine-0.4.0/README.md +89 -0
- frondori_engine-0.4.0/frondori_engine/__init__.py +25 -0
- frondori_engine-0.4.0/frondori_engine/registry.py +104 -0
- frondori_engine-0.4.0/frondori_engine/scene.py +34 -0
- frondori_engine-0.4.0/frondori_engine/testing.py +130 -0
- frondori_engine-0.4.0/frondori_engine/wire.py +89 -0
- frondori_engine-0.4.0/frondori_engine/worker.py +159 -0
- frondori_engine-0.4.0/frondori_engine.egg-info/PKG-INFO +120 -0
- frondori_engine-0.4.0/frondori_engine.egg-info/SOURCES.txt +17 -0
- frondori_engine-0.4.0/frondori_engine.egg-info/dependency_links.txt +1 -0
- frondori_engine-0.4.0/frondori_engine.egg-info/requires.txt +7 -0
- frondori_engine-0.4.0/frondori_engine.egg-info/top_level.txt +1 -0
- frondori_engine-0.4.0/pyproject.toml +43 -0
- frondori_engine-0.4.0/setup.cfg +4 -0
- frondori_engine-0.4.0/tests/test_registry.py +72 -0
- frondori_engine-0.4.0/tests/test_testing.py +67 -0
- frondori_engine-0.4.0/tests/test_worker.py +122 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Melvine Nargeot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: frondori-engine
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Frondori research environments: the common contract (PettingZoo API), registry, rendering format and contract tests. Environments are installed separately (frondori-football, frondori-kitchen...).
|
|
5
|
+
Author: Melvine Nargeot
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://frondori.com
|
|
8
|
+
Project-URL: Documentation, https://frondori.com/documentation/train-locally
|
|
9
|
+
Project-URL: Source, https://github.com/Melvin-klein/frondori-engine
|
|
10
|
+
Project-URL: Issues, https://github.com/Melvin-klein/frondori-engine/issues
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: gymnasium>=1.0
|
|
26
|
+
Requires-Dist: pettingzoo>=1.24
|
|
27
|
+
Requires-Dist: msgpack>=1.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# frondori-engine
|
|
33
|
+
|
|
34
|
+
Le contrat commun des environnements de recherche de
|
|
35
|
+
Frondori, au format [PettingZoo](https://pettingzoo.farama.org/).
|
|
36
|
+
|
|
37
|
+
**Ce paquet ne contient aucun environnement.** Chacun est un paquet à part,
|
|
38
|
+
dans son propre dépôt, qu'on installe seulement s'il nous intéresse :
|
|
39
|
+
|
|
40
|
+
| Paquet | Environnement |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `frondori-football` | `football-v0` — football 2D, duel (moteur Rust) |
|
|
43
|
+
| `frondori-kitchen` | `kitchen-v0` — cuisine coopérative |
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install frondori-engine frondori-kitchen
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
(Pas encore publiés sur PyPI : depuis un clone, `pip install -e .` dans
|
|
52
|
+
chaque dépôt.)
|
|
53
|
+
|
|
54
|
+
## Utilisation
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import frondori_engine
|
|
58
|
+
|
|
59
|
+
print(frondori_engine.registered_ids()) # les environnements installés
|
|
60
|
+
env = frondori_engine.make("kitchen-v0") # un pettingzoo.ParallelEnv
|
|
61
|
+
observations, infos = env.reset(seed=42)
|
|
62
|
+
|
|
63
|
+
while env.agents:
|
|
64
|
+
actions = {agent: env.action_space(agent).sample() for agent in env.agents}
|
|
65
|
+
observations, rewards, terminations, truncations, infos = env.step(actions)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Sans serveur, sans réseau, sans token. Compatible avec l'écosystème qui parle
|
|
69
|
+
PettingZoo (RLlib, TorchRL, CleanRL, SuperSuit...). Pour jouer un match en
|
|
70
|
+
local dans les conditions de la compétition, ou contre d'autres agents sur
|
|
71
|
+
le serveur, voir le SDK (`frondori-sdk`) : `Agent(..., local=True)`.
|
|
72
|
+
|
|
73
|
+
## Ce que contient ce paquet
|
|
74
|
+
|
|
75
|
+
- `frondori_engine.make` / `registered_ids` / `register` : le registre. Les
|
|
76
|
+
environnements installés sont découverts automatiquement par leur *entry
|
|
77
|
+
point* (groupe `frondori.environments`).
|
|
78
|
+
- `frondori_engine.scene` : le format de rendu commun (`render_mode="scene"`).
|
|
79
|
+
- `frondori_engine.wire` : le format des spaces et des valeurs sur le réseau.
|
|
80
|
+
- `frondori_engine.worker` : exécute un environnement pour le serveur de
|
|
81
|
+
compétition (un processus par match).
|
|
82
|
+
- `frondori_engine.testing` : les tests du contrat, à appeler depuis les
|
|
83
|
+
tests de chaque environnement (`check_environment("mon-jeu-v0")`).
|
|
84
|
+
|
|
85
|
+
## Créer un environnement
|
|
86
|
+
|
|
87
|
+
Un paquet Python qui dépend de `frondori-engine` et se déclare dans son
|
|
88
|
+
`pyproject.toml` :
|
|
89
|
+
|
|
90
|
+
```toml
|
|
91
|
+
[project.entry-points."frondori.environments"]
|
|
92
|
+
"mon-jeu-v0" = "mon_paquet:MonJeuEnv"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Guide complet, avec un exemple vérifié : Documentation > Create an
|
|
96
|
+
Environment, sur le site. L'exemple de ce guide (`tests/rps.py`) sert aussi
|
|
97
|
+
d'environnement de test à ce paquet.
|
|
98
|
+
|
|
99
|
+
## Développer / tester
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python -m venv .venv && source .venv/bin/activate
|
|
103
|
+
pip install -e ".[dev]"
|
|
104
|
+
python -m pytest
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Publier une version
|
|
108
|
+
|
|
109
|
+
1. Mettre à jour `version` dans `pyproject.toml` et commiter.
|
|
110
|
+
2. Pousser un tag du même numéro : `git tag v0.1.0 && git push origin v0.1.0`.
|
|
111
|
+
|
|
112
|
+
La CI (`.github/workflows/ci.yml`) teste, construit et publie sur PyPI ; elle
|
|
113
|
+
refuse un tag qui ne correspond pas à la version. Publication par *Trusted
|
|
114
|
+
Publishing*, sans token : à configurer une fois sur PyPI (projet `frondori-engine` >
|
|
115
|
+
Publishing > trusted publisher GitHub : ce dépôt, workflow `ci.yml`,
|
|
116
|
+
environnement `pypi`).
|
|
117
|
+
|
|
118
|
+
À publier EN PREMIER : les environnements et le SDK en dépendent.
|
|
119
|
+
|
|
120
|
+
Licence : MIT.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frondori-engine
|
|
2
|
+
|
|
3
|
+
Le contrat commun des environnements de recherche de
|
|
4
|
+
Frondori, au format [PettingZoo](https://pettingzoo.farama.org/).
|
|
5
|
+
|
|
6
|
+
**Ce paquet ne contient aucun environnement.** Chacun est un paquet à part,
|
|
7
|
+
dans son propre dépôt, qu'on installe seulement s'il nous intéresse :
|
|
8
|
+
|
|
9
|
+
| Paquet | Environnement |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `frondori-football` | `football-v0` — football 2D, duel (moteur Rust) |
|
|
12
|
+
| `frondori-kitchen` | `kitchen-v0` — cuisine coopérative |
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install frondori-engine frondori-kitchen
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
(Pas encore publiés sur PyPI : depuis un clone, `pip install -e .` dans
|
|
21
|
+
chaque dépôt.)
|
|
22
|
+
|
|
23
|
+
## Utilisation
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import frondori_engine
|
|
27
|
+
|
|
28
|
+
print(frondori_engine.registered_ids()) # les environnements installés
|
|
29
|
+
env = frondori_engine.make("kitchen-v0") # un pettingzoo.ParallelEnv
|
|
30
|
+
observations, infos = env.reset(seed=42)
|
|
31
|
+
|
|
32
|
+
while env.agents:
|
|
33
|
+
actions = {agent: env.action_space(agent).sample() for agent in env.agents}
|
|
34
|
+
observations, rewards, terminations, truncations, infos = env.step(actions)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Sans serveur, sans réseau, sans token. Compatible avec l'écosystème qui parle
|
|
38
|
+
PettingZoo (RLlib, TorchRL, CleanRL, SuperSuit...). Pour jouer un match en
|
|
39
|
+
local dans les conditions de la compétition, ou contre d'autres agents sur
|
|
40
|
+
le serveur, voir le SDK (`frondori-sdk`) : `Agent(..., local=True)`.
|
|
41
|
+
|
|
42
|
+
## Ce que contient ce paquet
|
|
43
|
+
|
|
44
|
+
- `frondori_engine.make` / `registered_ids` / `register` : le registre. Les
|
|
45
|
+
environnements installés sont découverts automatiquement par leur *entry
|
|
46
|
+
point* (groupe `frondori.environments`).
|
|
47
|
+
- `frondori_engine.scene` : le format de rendu commun (`render_mode="scene"`).
|
|
48
|
+
- `frondori_engine.wire` : le format des spaces et des valeurs sur le réseau.
|
|
49
|
+
- `frondori_engine.worker` : exécute un environnement pour le serveur de
|
|
50
|
+
compétition (un processus par match).
|
|
51
|
+
- `frondori_engine.testing` : les tests du contrat, à appeler depuis les
|
|
52
|
+
tests de chaque environnement (`check_environment("mon-jeu-v0")`).
|
|
53
|
+
|
|
54
|
+
## Créer un environnement
|
|
55
|
+
|
|
56
|
+
Un paquet Python qui dépend de `frondori-engine` et se déclare dans son
|
|
57
|
+
`pyproject.toml` :
|
|
58
|
+
|
|
59
|
+
```toml
|
|
60
|
+
[project.entry-points."frondori.environments"]
|
|
61
|
+
"mon-jeu-v0" = "mon_paquet:MonJeuEnv"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Guide complet, avec un exemple vérifié : Documentation > Create an
|
|
65
|
+
Environment, sur le site. L'exemple de ce guide (`tests/rps.py`) sert aussi
|
|
66
|
+
d'environnement de test à ce paquet.
|
|
67
|
+
|
|
68
|
+
## Développer / tester
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python -m venv .venv && source .venv/bin/activate
|
|
72
|
+
pip install -e ".[dev]"
|
|
73
|
+
python -m pytest
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Publier une version
|
|
77
|
+
|
|
78
|
+
1. Mettre à jour `version` dans `pyproject.toml` et commiter.
|
|
79
|
+
2. Pousser un tag du même numéro : `git tag v0.1.0 && git push origin v0.1.0`.
|
|
80
|
+
|
|
81
|
+
La CI (`.github/workflows/ci.yml`) teste, construit et publie sur PyPI ; elle
|
|
82
|
+
refuse un tag qui ne correspond pas à la version. Publication par *Trusted
|
|
83
|
+
Publishing*, sans token : à configurer une fois sur PyPI (projet `frondori-engine` >
|
|
84
|
+
Publishing > trusted publisher GitHub : ce dépôt, workflow `ci.yml`,
|
|
85
|
+
environnement `pypi`).
|
|
86
|
+
|
|
87
|
+
À publier EN PREMIER : les environnements et le SDK en dépendent.
|
|
88
|
+
|
|
89
|
+
Licence : MIT.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Le contrat commun des environnements de recherche Frondori.
|
|
2
|
+
|
|
3
|
+
Ce paquet ne contient AUCUN environnement : chacun est un paquet à part,
|
|
4
|
+
installé selon ses besoins, et découvert automatiquement :
|
|
5
|
+
|
|
6
|
+
pip install frondori-engine frondori-kitchen
|
|
7
|
+
|
|
8
|
+
import frondori_engine
|
|
9
|
+
|
|
10
|
+
print(frondori_engine.registered_ids()) # ['kitchen-v0']
|
|
11
|
+
env = frondori_engine.make("kitchen-v0")
|
|
12
|
+
observations, infos = env.reset(seed=42)
|
|
13
|
+
while env.agents:
|
|
14
|
+
actions = {agent: env.action_space(agent).sample() for agent in env.agents}
|
|
15
|
+
observations, rewards, terminations, truncations, infos = env.step(actions)
|
|
16
|
+
|
|
17
|
+
Il fournit ce que tous partagent : le registre (`make`, `register`), le
|
|
18
|
+
format des scènes (`scene`), celui des spaces et des valeurs sur le réseau
|
|
19
|
+
(`wire`), le worker qui exécute un environnement pour le serveur de
|
|
20
|
+
compétition (`worker`) et les tests du contrat (`testing`).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from frondori_engine.registry import ENTRY_POINT_GROUP, EnvSpec, get_spec, make, register, registered_ids
|
|
24
|
+
|
|
25
|
+
__all__ = ["ENTRY_POINT_GROUP", "EnvSpec", "get_spec", "make", "register", "registered_ids"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Registre des environnements, sur le modèle de `gymnasium.make`.
|
|
2
|
+
|
|
3
|
+
Les environnements ne vivent PAS dans ce paquet : chacun est un paquet
|
|
4
|
+
Python indépendant (`frondori-football`, `frondori-kitchen`...), dans son
|
|
5
|
+
propre dépôt, qu'un chercheur installe seulement s'il l'intéresse. Un
|
|
6
|
+
paquet se déclare par un *entry point* du groupe `frondori.environments`,
|
|
7
|
+
dans son `pyproject.toml` :
|
|
8
|
+
|
|
9
|
+
[project.entry-points."frondori.environments"]
|
|
10
|
+
"kitchen-v0" = "frondori_kitchen:KitchenEnv"
|
|
11
|
+
|
|
12
|
+
Au premier appel à `make`/`registered_ids`, le registre découvre tous les
|
|
13
|
+
environnements installés (`importlib.metadata.entry_points`, le mécanisme
|
|
14
|
+
standard des plugins Python, celui de pytest ou de Gymnasium). Chaque classe
|
|
15
|
+
n'est importée qu'au moment de créer l'environnement : lister le catalogue
|
|
16
|
+
ne charge aucun moteur.
|
|
17
|
+
|
|
18
|
+
`register` reste disponible pour un environnement en cours d'écriture, pas
|
|
19
|
+
encore empaqueté.
|
|
20
|
+
|
|
21
|
+
Tout identifiant porte une version (`nom-vN`). Changer les règles d'un
|
|
22
|
+
environnement, c'est en publier une nouvelle version, jamais modifier
|
|
23
|
+
l'ancienne : un résultat, un replay ou un classement obtenu sur
|
|
24
|
+
`football-v0` doit rester comparable dans six mois.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from importlib.metadata import EntryPoint, entry_points
|
|
32
|
+
from typing import Any, Callable
|
|
33
|
+
|
|
34
|
+
from pettingzoo import ParallelEnv
|
|
35
|
+
|
|
36
|
+
ENTRY_POINT_GROUP = "frondori.environments"
|
|
37
|
+
|
|
38
|
+
_ID_PATTERN = re.compile(r"^[a-z][a-z0-9_]*-v\d+$")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class EnvSpec:
|
|
43
|
+
id: str
|
|
44
|
+
entry_point: Callable[..., ParallelEnv]
|
|
45
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
46
|
+
# Paquet installable qui fournit l'environnement (ex. "frondori-kitchen"),
|
|
47
|
+
# `None` pour un environnement enregistré à la main. Publié avec le
|
|
48
|
+
# catalogue : le site en tire la commande d'installation.
|
|
49
|
+
package: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_registry: dict[str, EnvSpec] = {}
|
|
53
|
+
_discovered = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def register(id: str, entry_point: Callable[..., ParallelEnv], package: str | None = None, **kwargs: Any) -> None:
|
|
57
|
+
"""Enregistre un environnement. `kwargs` : paramètres par défaut passés à
|
|
58
|
+
`entry_point`, surchargeables au moment de `make`."""
|
|
59
|
+
if not _ID_PATTERN.match(id):
|
|
60
|
+
raise ValueError(f"identifiant invalide {id!r} : format attendu 'nom-vN' (ex. 'football-v0')")
|
|
61
|
+
if id in _registry:
|
|
62
|
+
raise ValueError(f"environnement {id!r} déjà enregistré ({_registry[id].package or 'register()'})")
|
|
63
|
+
_registry[id] = EnvSpec(id=id, entry_point=entry_point, kwargs=kwargs, package=package)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def make(id: str, **kwargs: Any) -> ParallelEnv:
|
|
67
|
+
try:
|
|
68
|
+
spec = get_spec(id)
|
|
69
|
+
except KeyError:
|
|
70
|
+
known = ", ".join(registered_ids()) or "(aucun : installer un paquet d'environnement, ex. frondori-kitchen)"
|
|
71
|
+
raise KeyError(f"environnement inconnu {id!r} ; disponibles : {known}") from None
|
|
72
|
+
return spec.entry_point(**{**spec.kwargs, **kwargs})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_spec(id: str) -> EnvSpec:
|
|
76
|
+
_discover()
|
|
77
|
+
return _registry[id]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def registered_ids() -> list[str]:
|
|
81
|
+
_discover()
|
|
82
|
+
return sorted(_registry)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _discover() -> None:
|
|
86
|
+
"""Enregistre les environnements des paquets installés, une seule fois.
|
|
87
|
+
Deux paquets qui déclarent le même identifiant : erreur explicite plutôt
|
|
88
|
+
qu'un choix silencieux de l'un des deux."""
|
|
89
|
+
global _discovered
|
|
90
|
+
if _discovered:
|
|
91
|
+
return
|
|
92
|
+
_discovered = True
|
|
93
|
+
for entry in entry_points(group=ENTRY_POINT_GROUP):
|
|
94
|
+
register(entry.name, _lazy(entry), package=entry.dist.name if entry.dist else None)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _lazy(entry: EntryPoint) -> Callable[..., ParallelEnv]:
|
|
98
|
+
# La classe n'est importée qu'à la création d'un environnement : lister
|
|
99
|
+
# les environnements disponibles ne charge aucun moteur (le football
|
|
100
|
+
# charge un module natif).
|
|
101
|
+
def create(**kwargs: Any) -> ParallelEnv:
|
|
102
|
+
return entry.load()(**kwargs)
|
|
103
|
+
|
|
104
|
+
return create
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Format de rendu commun à tous les environnements (`render_mode="scene"`).
|
|
2
|
+
|
|
3
|
+
Un environnement ne dessine jamais lui-même : `render()` renvoie une scène
|
|
4
|
+
faite de primitives simples (rectangles, cercles, texte), sérialisable en
|
|
5
|
+
JSON telle quelle. N'importe quel afficheur — un script matplotlib local, la
|
|
6
|
+
page de match du site — peut ainsi dessiner n'importe quel environnement
|
|
7
|
+
sans rien connaître de ses règles.
|
|
8
|
+
|
|
9
|
+
Repère : origine en haut à gauche, x vers la droite, y vers le bas, dans les
|
|
10
|
+
unités propres à l'environnement (mètres pour le football, cases pour la
|
|
11
|
+
cuisine). `width`/`height` donnent l'étendue de la scène, pour la mise à
|
|
12
|
+
l'échelle. Les couleurs sont des chaînes CSS.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def scene(width: float, height: float, shapes: list[dict], background: str = "#ffffff") -> dict:
|
|
19
|
+
return {"width": float(width), "height": float(height), "background": background, "shapes": shapes}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def rect(x: float, y: float, w: float, h: float, fill: str) -> dict:
|
|
23
|
+
"""Rectangle plein ; `(x, y)` est son coin haut-gauche."""
|
|
24
|
+
return {"type": "rect", "x": float(x), "y": float(y), "w": float(w), "h": float(h), "fill": fill}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def circle(x: float, y: float, r: float, fill: str) -> dict:
|
|
28
|
+
"""Disque plein ; `(x, y)` est son centre."""
|
|
29
|
+
return {"type": "circle", "x": float(x), "y": float(y), "r": float(r), "fill": fill}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def text(x: float, y: float, value: str, fill: str = "#0f172a", size: float = 0.5) -> dict:
|
|
33
|
+
"""Texte centré sur `(x, y)` ; `size` est la hauteur des caractères, dans les unités de la scène."""
|
|
34
|
+
return {"type": "text", "x": float(x), "y": float(y), "text": value, "fill": fill, "size": float(size)}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Les tests du contrat commun, réutilisables par chaque paquet
|
|
2
|
+
d'environnement dans ses propres tests :
|
|
3
|
+
|
|
4
|
+
from frondori_engine.testing import check_environment
|
|
5
|
+
|
|
6
|
+
def test_contract():
|
|
7
|
+
check_environment("kitchen-v0")
|
|
8
|
+
|
|
9
|
+
Chaque vérification est aussi disponible seule (`check_*`). Elles lèvent
|
|
10
|
+
`AssertionError` (ou l'erreur de PettingZoo) au premier manquement, avec un
|
|
11
|
+
message qui dit quoi corriger.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
from pettingzoo.test import parallel_api_test, parallel_seed_test
|
|
19
|
+
|
|
20
|
+
import frondori_engine
|
|
21
|
+
from frondori_engine import wire
|
|
22
|
+
|
|
23
|
+
RANKINGS = ("elo", "mean_return")
|
|
24
|
+
SHAPE_TYPES = {"rect", "circle", "text"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_environment(env_id: str) -> None:
|
|
28
|
+
"""Tout le contrat : à appeler depuis les tests de chaque environnement."""
|
|
29
|
+
check_pettingzoo_api(env_id)
|
|
30
|
+
check_reproducible(env_id)
|
|
31
|
+
check_observations_fit_their_space(env_id)
|
|
32
|
+
check_metadata(env_id)
|
|
33
|
+
check_spaces_can_be_sent(env_id)
|
|
34
|
+
check_neutral_action(env_id)
|
|
35
|
+
check_scene(env_id)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_pettingzoo_api(env_id: str) -> None:
|
|
39
|
+
"""Le test officiel de PettingZoo (API parallèle). Il exige notamment
|
|
40
|
+
que `observation_space(agent)` renvoie le MÊME objet à chaque appel :
|
|
41
|
+
créer les spaces une fois, dans `__init__`."""
|
|
42
|
+
parallel_api_test(frondori_engine.make(env_id), num_cycles=300)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_reproducible(env_id: str) -> None:
|
|
46
|
+
"""Même seed + mêmes actions = même épisode."""
|
|
47
|
+
parallel_seed_test(lambda: frondori_engine.make(env_id), num_cycles=100)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_observations_fit_their_space(env_id: str, steps: int = 500) -> None:
|
|
51
|
+
# `parallel_api_test` ne le vérifie PAS (constaté : il laisse passer une
|
|
52
|
+
# valeur hors bornes). Or c'est sur ces spaces que le serveur et les
|
|
53
|
+
# agents s'appuient : vérifié ici explicitement.
|
|
54
|
+
env = frondori_engine.make(env_id)
|
|
55
|
+
observations, _ = env.reset(seed=0)
|
|
56
|
+
for _ in range(steps):
|
|
57
|
+
for agent, observation in observations.items():
|
|
58
|
+
assert env.observation_space(agent).contains(observation), (
|
|
59
|
+
f"{env_id} : l'observation de {agent} sort de son observation_space : {observation!r}"
|
|
60
|
+
)
|
|
61
|
+
if not env.agents:
|
|
62
|
+
break
|
|
63
|
+
observations, *_ = env.step({agent: env.action_space(agent).sample() for agent in env.agents})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def check_metadata(env_id: str) -> None:
|
|
67
|
+
"""Les métadonnées dont le serveur et le site ont besoin."""
|
|
68
|
+
env = frondori_engine.make(env_id)
|
|
69
|
+
metadata = env.metadata
|
|
70
|
+
for key in ("title", "description", "documentation"):
|
|
71
|
+
assert isinstance(metadata.get(key), str) and metadata[key].strip(), (
|
|
72
|
+
f"{env_id} : metadata[{key!r}] (texte non vide) est requis"
|
|
73
|
+
)
|
|
74
|
+
assert (metadata.get("render_fps") or 0) > 0, f"{env_id} : metadata['render_fps'] (pas par seconde) est requis"
|
|
75
|
+
assert (metadata.get("compute_budget_ms") or 0) > 0, (
|
|
76
|
+
f"{env_id} : metadata['compute_budget_ms'] (temps de calcul par action, en ms) est requis"
|
|
77
|
+
)
|
|
78
|
+
assert metadata.get("ranking") in RANKINGS, f"{env_id} : metadata['ranking'] doit valoir l'un de {RANKINGS}"
|
|
79
|
+
if metadata["ranking"] == "elo":
|
|
80
|
+
assert len(env.possible_agents) == 2, f"{env_id} : un classement ELO n'a de sens qu'en duel (2 agents)"
|
|
81
|
+
assert "scene" in metadata.get("render_modes", []), f"{env_id} : render_mode 'scene' est requis"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_spaces_can_be_sent(env_id: str) -> None:
|
|
85
|
+
"""Seuls Box, Discrete, MultiDiscrete et Dict passent sur le réseau."""
|
|
86
|
+
env = frondori_engine.make(env_id)
|
|
87
|
+
for agent in env.possible_agents:
|
|
88
|
+
wire.space_to_spec(env.observation_space(agent))
|
|
89
|
+
wire.space_to_spec(env.action_space(agent))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def check_neutral_action(env_id: str) -> None:
|
|
93
|
+
"""L'élément "zéro" de chaque action_space est une action valide : c'est
|
|
94
|
+
celle que le serveur joue pour un agent dont l'action est invalide, trop
|
|
95
|
+
lente ou absente (elle doit ne rien faire — ce que seul l'auteur de
|
|
96
|
+
l'environnement peut garantir)."""
|
|
97
|
+
env = frondori_engine.make(env_id)
|
|
98
|
+
env.reset(seed=0)
|
|
99
|
+
for agent in env.possible_agents:
|
|
100
|
+
space = env.action_space(agent)
|
|
101
|
+
assert space.contains(wire.neutral_action(space)), (
|
|
102
|
+
f"{env_id} : l'action neutre de {agent} sort de son action_space"
|
|
103
|
+
)
|
|
104
|
+
env.step({agent: wire.neutral_action(env.action_space(agent)) for agent in env.agents})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def check_scene(env_id: str) -> None:
|
|
108
|
+
"""Le rendu générique : sérialisable en JSON, primitives connues."""
|
|
109
|
+
env = frondori_engine.make(env_id, render_mode="scene")
|
|
110
|
+
env.reset(seed=0)
|
|
111
|
+
env.step({agent: env.action_space(agent).sample() for agent in env.agents})
|
|
112
|
+
scene = env.render()
|
|
113
|
+
|
|
114
|
+
json.dumps(scene)
|
|
115
|
+
assert scene["width"] > 0 and scene["height"] > 0, f"{env_id} : la scène doit avoir une largeur et une hauteur"
|
|
116
|
+
assert scene["shapes"], f"{env_id} : la scène est vide"
|
|
117
|
+
unknown = {shape["type"] for shape in scene["shapes"]} - SHAPE_TYPES
|
|
118
|
+
assert not unknown, f"{env_id} : primitives de scène inconnues {unknown} (attendu : {SHAPE_TYPES})"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
"check_environment",
|
|
123
|
+
"check_metadata",
|
|
124
|
+
"check_neutral_action",
|
|
125
|
+
"check_observations_fit_their_space",
|
|
126
|
+
"check_pettingzoo_api",
|
|
127
|
+
"check_reproducible",
|
|
128
|
+
"check_scene",
|
|
129
|
+
"check_spaces_can_be_sent",
|
|
130
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Passage entre les valeurs d'un environnement (numpy, spaces Gymnasium) et
|
|
2
|
+
leur forme sur le fil (types MessagePack natifs : nombres, listes, dicts).
|
|
3
|
+
|
|
4
|
+
Ce format fait partie du protocole de compétition : le SDK
|
|
5
|
+
(`frondori-sdk-python`) en implémente l'autre moitié (spec -> space, fil ->
|
|
6
|
+
numpy). Toute modification ici doit y être reportée.
|
|
7
|
+
|
|
8
|
+
Spaces supportés : Box, Discrete, MultiDiscrete, et Dict de ces spaces. Un
|
|
9
|
+
environnement qui en utilise un autre est refusé explicitement à la
|
|
10
|
+
description (`space_to_spec`), pas silencieusement mal transmis.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from gymnasium import spaces
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def space_to_spec(space: spaces.Space) -> dict:
|
|
20
|
+
if isinstance(space, spaces.Box):
|
|
21
|
+
return {
|
|
22
|
+
"type": "box",
|
|
23
|
+
"shape": list(space.shape),
|
|
24
|
+
"dtype": space.dtype.name,
|
|
25
|
+
"low": space.low.tolist(),
|
|
26
|
+
"high": space.high.tolist(),
|
|
27
|
+
}
|
|
28
|
+
if isinstance(space, spaces.Discrete):
|
|
29
|
+
return {"type": "discrete", "n": int(space.n), "start": int(space.start)}
|
|
30
|
+
if isinstance(space, spaces.MultiDiscrete):
|
|
31
|
+
return {
|
|
32
|
+
"type": "multi_discrete",
|
|
33
|
+
"nvec": space.nvec.tolist(),
|
|
34
|
+
"start": space.start.tolist(),
|
|
35
|
+
"dtype": space.dtype.name,
|
|
36
|
+
}
|
|
37
|
+
if isinstance(space, spaces.Dict):
|
|
38
|
+
return {"type": "dict", "spaces": {key: space_to_spec(sub) for key, sub in space.spaces.items()}}
|
|
39
|
+
raise TypeError(f"space non supporté par le protocole : {type(space).__name__}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def to_wire(value):
|
|
43
|
+
"""numpy -> types natifs, récursivement (observations, infos)."""
|
|
44
|
+
if isinstance(value, np.ndarray):
|
|
45
|
+
return value.tolist()
|
|
46
|
+
if isinstance(value, np.generic):
|
|
47
|
+
return value.item()
|
|
48
|
+
if isinstance(value, dict):
|
|
49
|
+
return {str(key): to_wire(item) for key, item in value.items()}
|
|
50
|
+
if isinstance(value, (list, tuple)):
|
|
51
|
+
return [to_wire(item) for item in value]
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def from_wire(space: spaces.Space, value):
|
|
56
|
+
"""Action reçue du fil -> valeur du space. Lève `ValueError` si elle n'y
|
|
57
|
+
appartient pas : un agent n'est pas de confiance, rien n'est passé à
|
|
58
|
+
l'environnement sans vérification."""
|
|
59
|
+
if isinstance(space, spaces.Dict):
|
|
60
|
+
if not isinstance(value, dict) or set(value) != set(space.spaces):
|
|
61
|
+
raise ValueError(f"attendu un dict avec les clés {sorted(space.spaces)}")
|
|
62
|
+
return {key: from_wire(sub, value[key]) for key, sub in space.spaces.items()}
|
|
63
|
+
if isinstance(space, spaces.Discrete):
|
|
64
|
+
# `bool` est un sous-type d'`int` en Python : refusé explicitement.
|
|
65
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
66
|
+
raise ValueError("attendu un entier")
|
|
67
|
+
action = np.int64(value)
|
|
68
|
+
else:
|
|
69
|
+
action = np.asarray(value, dtype=space.dtype)
|
|
70
|
+
if not space.contains(action):
|
|
71
|
+
raise ValueError("valeur hors de l'action_space")
|
|
72
|
+
return action
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def neutral_action(space: spaces.Space):
|
|
76
|
+
"""Action appliquée à un agent qui n'a pas répondu à temps, ou dont
|
|
77
|
+
l'action est invalide : l'élément « zéro » du space (0 ramené dans les
|
|
78
|
+
bornes, ou la plus petite valeur discrète). Un environnement doit donc
|
|
79
|
+
être conçu pour que cet élément soit une action sans effet — c'est le cas
|
|
80
|
+
du football (ne pas bouger, ne pas tirer) et de la cuisine (rester)."""
|
|
81
|
+
if isinstance(space, spaces.Dict):
|
|
82
|
+
return {key: neutral_action(sub) for key, sub in space.spaces.items()}
|
|
83
|
+
if isinstance(space, spaces.Discrete):
|
|
84
|
+
return np.int64(space.start)
|
|
85
|
+
if isinstance(space, spaces.MultiDiscrete):
|
|
86
|
+
return np.array(space.start, dtype=space.dtype)
|
|
87
|
+
if isinstance(space, spaces.Box):
|
|
88
|
+
return np.clip(np.zeros(space.shape), space.low, space.high).astype(space.dtype)
|
|
89
|
+
raise TypeError(f"space non supporté par le protocole : {type(space).__name__}")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Exécute un environnement dans un processus dédié, piloté par le serveur de
|
|
2
|
+
compétition (`frondori-server`) via l'entrée et la sortie standard.
|
|
3
|
+
|
|
4
|
+
Le serveur lance un worker par match (`python -m frondori_engine.worker`) :
|
|
5
|
+
un environnement qui plante ou se bloque n'emporte que son propre match, et
|
|
6
|
+
le serveur n'a pas à savoir en quel langage l'environnement est écrit.
|
|
7
|
+
|
|
8
|
+
Chaque message est un objet MessagePack précédé de sa taille (4 octets,
|
|
9
|
+
big-endian), dans les deux sens. Requêtes, selon `cmd` :
|
|
10
|
+
- `describe` : catalogue des environnements (agents, cadence, budget de
|
|
11
|
+
calcul, classement, spaces) ;
|
|
12
|
+
- `start` (`env_id`, `seed`) : crée l'environnement et renvoie les
|
|
13
|
+
observations initiales ;
|
|
14
|
+
- `step` (`actions` : agent -> action, ou `nil` si l'agent n'a pas répondu à
|
|
15
|
+
temps) : un pas de simulation.
|
|
16
|
+
Réponses : `{"ok": true, ...}` ou `{"ok": false, "error": "..."}`.
|
|
17
|
+
|
|
18
|
+
Une action absente ou invalide est remplacée par l'action neutre de son
|
|
19
|
+
space (`wire.neutral_action`) ; les agents dont l'action a été refusée sont
|
|
20
|
+
listés dans `rejected`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import struct
|
|
28
|
+
import sys
|
|
29
|
+
import traceback
|
|
30
|
+
|
|
31
|
+
import msgpack
|
|
32
|
+
|
|
33
|
+
import frondori_engine
|
|
34
|
+
from frondori_engine import wire
|
|
35
|
+
|
|
36
|
+
# Comment la plateforme classe les agents d'un environnement (déclaré dans
|
|
37
|
+
# `metadata["ranking"]`) : `elo` pour un duel (le vainqueur est l'agent au
|
|
38
|
+
# meilleur retour), `mean_return` pour le reste (coopératif, solo) — le
|
|
39
|
+
# retour moyen de l'agent par match.
|
|
40
|
+
RANKINGS = ("elo", "mean_return")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main() -> None:
|
|
44
|
+
# La sortie standard porte le protocole : un `print` égaré dans le code
|
|
45
|
+
# d'un environnement la corromprait. On garde une copie privée du vrai
|
|
46
|
+
# stdout pour les réponses, et tout le reste part sur stderr.
|
|
47
|
+
protocol_out = os.fdopen(os.dup(1), "wb")
|
|
48
|
+
os.dup2(2, 1)
|
|
49
|
+
sys.stdout = sys.stderr
|
|
50
|
+
|
|
51
|
+
session = _Session()
|
|
52
|
+
while (request := _read_frame(sys.stdin.buffer)) is not None:
|
|
53
|
+
try:
|
|
54
|
+
reply = {"ok": True, **session.handle(request)}
|
|
55
|
+
except Exception:
|
|
56
|
+
reply = {"ok": False, "error": traceback.format_exc()}
|
|
57
|
+
_write_frame(protocol_out, reply)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _Session:
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self._env = None
|
|
63
|
+
|
|
64
|
+
def handle(self, request: dict) -> dict:
|
|
65
|
+
command = request.get("cmd")
|
|
66
|
+
if command == "describe":
|
|
67
|
+
return {"environments": {env_id: _describe(env_id) for env_id in frondori_engine.registered_ids()}}
|
|
68
|
+
if command == "start":
|
|
69
|
+
return self._start(request["env_id"], request["seed"])
|
|
70
|
+
if command == "step":
|
|
71
|
+
return self._step(request["actions"])
|
|
72
|
+
raise ValueError(f"commande inconnue : {command!r}")
|
|
73
|
+
|
|
74
|
+
def _start(self, env_id: str, seed: int) -> dict:
|
|
75
|
+
self._env = frondori_engine.make(env_id, render_mode="scene")
|
|
76
|
+
observations, infos = self._env.reset(seed=seed)
|
|
77
|
+
return {
|
|
78
|
+
"agents": list(self._env.possible_agents),
|
|
79
|
+
"observations": wire.to_wire(observations),
|
|
80
|
+
"infos": wire.to_wire(infos),
|
|
81
|
+
"scene": self._scene(),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def _step(self, received: dict) -> dict:
|
|
85
|
+
actions, rejected = {}, []
|
|
86
|
+
for agent in self._env.agents:
|
|
87
|
+
space = self._env.action_space(agent)
|
|
88
|
+
value = received.get(agent)
|
|
89
|
+
try:
|
|
90
|
+
if value is None:
|
|
91
|
+
raise ValueError("pas d'action")
|
|
92
|
+
actions[agent] = wire.from_wire(space, value)
|
|
93
|
+
except (ValueError, TypeError):
|
|
94
|
+
actions[agent] = wire.neutral_action(space)
|
|
95
|
+
if value is not None:
|
|
96
|
+
rejected.append(agent)
|
|
97
|
+
|
|
98
|
+
observations, rewards, terminations, truncations, infos = self._env.step(actions)
|
|
99
|
+
return {
|
|
100
|
+
"observations": wire.to_wire(observations),
|
|
101
|
+
"rewards": {agent: float(reward) for agent, reward in rewards.items()},
|
|
102
|
+
"terminations": {agent: bool(done) for agent, done in terminations.items()},
|
|
103
|
+
"truncations": {agent: bool(done) for agent, done in truncations.items()},
|
|
104
|
+
"infos": wire.to_wire(infos),
|
|
105
|
+
"scene": self._scene(),
|
|
106
|
+
"rejected": rejected,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def _scene(self) -> str:
|
|
110
|
+
# Déjà en JSON : le serveur la relaie telle quelle aux spectateurs et
|
|
111
|
+
# la concatène dans le replay, sans jamais avoir à la décoder.
|
|
112
|
+
return json.dumps(self._env.render(), separators=(",", ":"))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _describe(env_id: str) -> dict:
|
|
116
|
+
env = frondori_engine.make(env_id)
|
|
117
|
+
tick_rate = env.metadata.get("render_fps")
|
|
118
|
+
if not tick_rate or tick_rate <= 0:
|
|
119
|
+
raise ValueError(f"{env_id} : metadata['render_fps'] (cadence en pas/seconde) est requis")
|
|
120
|
+
ranking = env.metadata.get("ranking")
|
|
121
|
+
if ranking not in RANKINGS:
|
|
122
|
+
raise ValueError(f"{env_id} : metadata['ranking'] doit valoir l'un de {RANKINGS}")
|
|
123
|
+
if ranking == "elo" and len(env.possible_agents) != 2:
|
|
124
|
+
raise ValueError(f"{env_id} : un classement ELO n'a de sens qu'en duel (2 agents)")
|
|
125
|
+
budget = env.metadata.get("compute_budget_ms")
|
|
126
|
+
if not budget or budget <= 0:
|
|
127
|
+
raise ValueError(f"{env_id} : metadata['compute_budget_ms'] (temps de calcul par action, en ms) est requis")
|
|
128
|
+
return {
|
|
129
|
+
"agents": list(env.possible_agents),
|
|
130
|
+
"tick_rate": float(tick_rate),
|
|
131
|
+
"ranking": ranking,
|
|
132
|
+
"compute_budget_ms": float(budget),
|
|
133
|
+
"title": env.metadata.get("title", env_id),
|
|
134
|
+
"description": env.metadata.get("description", ""),
|
|
135
|
+
"documentation": env.metadata.get("documentation", ""),
|
|
136
|
+
# Le paquet à installer pour jouer à cet environnement en local
|
|
137
|
+
# (le site affiche la commande `pip install`).
|
|
138
|
+
"package": frondori_engine.get_spec(env_id).package or "",
|
|
139
|
+
"observation_spaces": {agent: wire.space_to_spec(env.observation_space(agent)) for agent in env.possible_agents},
|
|
140
|
+
"action_spaces": {agent: wire.space_to_spec(env.action_space(agent)) for agent in env.possible_agents},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _read_frame(stream):
|
|
145
|
+
header = stream.read(4)
|
|
146
|
+
if len(header) < 4:
|
|
147
|
+
return None
|
|
148
|
+
(size,) = struct.unpack(">I", header)
|
|
149
|
+
return msgpack.unpackb(stream.read(size), raw=False)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _write_frame(stream, message: dict) -> None:
|
|
153
|
+
payload = msgpack.packb(message, use_bin_type=True)
|
|
154
|
+
stream.write(struct.pack(">I", len(payload)) + payload)
|
|
155
|
+
stream.flush()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: frondori-engine
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Frondori research environments: the common contract (PettingZoo API), registry, rendering format and contract tests. Environments are installed separately (frondori-football, frondori-kitchen...).
|
|
5
|
+
Author: Melvine Nargeot
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://frondori.com
|
|
8
|
+
Project-URL: Documentation, https://frondori.com/documentation/train-locally
|
|
9
|
+
Project-URL: Source, https://github.com/Melvin-klein/frondori-engine
|
|
10
|
+
Project-URL: Issues, https://github.com/Melvin-klein/frondori-engine/issues
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: gymnasium>=1.0
|
|
26
|
+
Requires-Dist: pettingzoo>=1.24
|
|
27
|
+
Requires-Dist: msgpack>=1.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# frondori-engine
|
|
33
|
+
|
|
34
|
+
Le contrat commun des environnements de recherche de
|
|
35
|
+
Frondori, au format [PettingZoo](https://pettingzoo.farama.org/).
|
|
36
|
+
|
|
37
|
+
**Ce paquet ne contient aucun environnement.** Chacun est un paquet à part,
|
|
38
|
+
dans son propre dépôt, qu'on installe seulement s'il nous intéresse :
|
|
39
|
+
|
|
40
|
+
| Paquet | Environnement |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `frondori-football` | `football-v0` — football 2D, duel (moteur Rust) |
|
|
43
|
+
| `frondori-kitchen` | `kitchen-v0` — cuisine coopérative |
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install frondori-engine frondori-kitchen
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
(Pas encore publiés sur PyPI : depuis un clone, `pip install -e .` dans
|
|
52
|
+
chaque dépôt.)
|
|
53
|
+
|
|
54
|
+
## Utilisation
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import frondori_engine
|
|
58
|
+
|
|
59
|
+
print(frondori_engine.registered_ids()) # les environnements installés
|
|
60
|
+
env = frondori_engine.make("kitchen-v0") # un pettingzoo.ParallelEnv
|
|
61
|
+
observations, infos = env.reset(seed=42)
|
|
62
|
+
|
|
63
|
+
while env.agents:
|
|
64
|
+
actions = {agent: env.action_space(agent).sample() for agent in env.agents}
|
|
65
|
+
observations, rewards, terminations, truncations, infos = env.step(actions)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Sans serveur, sans réseau, sans token. Compatible avec l'écosystème qui parle
|
|
69
|
+
PettingZoo (RLlib, TorchRL, CleanRL, SuperSuit...). Pour jouer un match en
|
|
70
|
+
local dans les conditions de la compétition, ou contre d'autres agents sur
|
|
71
|
+
le serveur, voir le SDK (`frondori-sdk`) : `Agent(..., local=True)`.
|
|
72
|
+
|
|
73
|
+
## Ce que contient ce paquet
|
|
74
|
+
|
|
75
|
+
- `frondori_engine.make` / `registered_ids` / `register` : le registre. Les
|
|
76
|
+
environnements installés sont découverts automatiquement par leur *entry
|
|
77
|
+
point* (groupe `frondori.environments`).
|
|
78
|
+
- `frondori_engine.scene` : le format de rendu commun (`render_mode="scene"`).
|
|
79
|
+
- `frondori_engine.wire` : le format des spaces et des valeurs sur le réseau.
|
|
80
|
+
- `frondori_engine.worker` : exécute un environnement pour le serveur de
|
|
81
|
+
compétition (un processus par match).
|
|
82
|
+
- `frondori_engine.testing` : les tests du contrat, à appeler depuis les
|
|
83
|
+
tests de chaque environnement (`check_environment("mon-jeu-v0")`).
|
|
84
|
+
|
|
85
|
+
## Créer un environnement
|
|
86
|
+
|
|
87
|
+
Un paquet Python qui dépend de `frondori-engine` et se déclare dans son
|
|
88
|
+
`pyproject.toml` :
|
|
89
|
+
|
|
90
|
+
```toml
|
|
91
|
+
[project.entry-points."frondori.environments"]
|
|
92
|
+
"mon-jeu-v0" = "mon_paquet:MonJeuEnv"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Guide complet, avec un exemple vérifié : Documentation > Create an
|
|
96
|
+
Environment, sur le site. L'exemple de ce guide (`tests/rps.py`) sert aussi
|
|
97
|
+
d'environnement de test à ce paquet.
|
|
98
|
+
|
|
99
|
+
## Développer / tester
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python -m venv .venv && source .venv/bin/activate
|
|
103
|
+
pip install -e ".[dev]"
|
|
104
|
+
python -m pytest
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Publier une version
|
|
108
|
+
|
|
109
|
+
1. Mettre à jour `version` dans `pyproject.toml` et commiter.
|
|
110
|
+
2. Pousser un tag du même numéro : `git tag v0.1.0 && git push origin v0.1.0`.
|
|
111
|
+
|
|
112
|
+
La CI (`.github/workflows/ci.yml`) teste, construit et publie sur PyPI ; elle
|
|
113
|
+
refuse un tag qui ne correspond pas à la version. Publication par *Trusted
|
|
114
|
+
Publishing*, sans token : à configurer une fois sur PyPI (projet `frondori-engine` >
|
|
115
|
+
Publishing > trusted publisher GitHub : ce dépôt, workflow `ci.yml`,
|
|
116
|
+
environnement `pypi`).
|
|
117
|
+
|
|
118
|
+
À publier EN PREMIER : les environnements et le SDK en dépendent.
|
|
119
|
+
|
|
120
|
+
Licence : MIT.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
frondori_engine/__init__.py
|
|
5
|
+
frondori_engine/registry.py
|
|
6
|
+
frondori_engine/scene.py
|
|
7
|
+
frondori_engine/testing.py
|
|
8
|
+
frondori_engine/wire.py
|
|
9
|
+
frondori_engine/worker.py
|
|
10
|
+
frondori_engine.egg-info/PKG-INFO
|
|
11
|
+
frondori_engine.egg-info/SOURCES.txt
|
|
12
|
+
frondori_engine.egg-info/dependency_links.txt
|
|
13
|
+
frondori_engine.egg-info/requires.txt
|
|
14
|
+
frondori_engine.egg-info/top_level.txt
|
|
15
|
+
tests/test_registry.py
|
|
16
|
+
tests/test_testing.py
|
|
17
|
+
tests/test_worker.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
frondori_engine
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "frondori-engine"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Frondori research environments: the common contract (PettingZoo API), registry, rendering format and contract tests. Environments are installed separately (frondori-football, frondori-kitchen...)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Melvine Nargeot" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Intended Audience :: Education",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"numpy>=1.24",
|
|
28
|
+
"gymnasium>=1.0",
|
|
29
|
+
"pettingzoo>=1.24",
|
|
30
|
+
"msgpack>=1.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://frondori.com"
|
|
35
|
+
Documentation = "https://frondori.com/documentation/train-locally"
|
|
36
|
+
Source = "https://github.com/Melvin-klein/frondori-engine"
|
|
37
|
+
Issues = "https://github.com/Melvin-klein/frondori-engine/issues"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = ["pytest>=7"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools]
|
|
43
|
+
packages = ["frondori_engine"]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from importlib.metadata import EntryPoint
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import frondori_engine
|
|
6
|
+
from frondori_engine import registry
|
|
7
|
+
from rps import RockPaperScissorsEnv
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_make_passes_kwargs_to_the_environment():
|
|
11
|
+
env = frondori_engine.make("rps-v0", rounds=3)
|
|
12
|
+
env.reset()
|
|
13
|
+
|
|
14
|
+
for _ in range(3):
|
|
15
|
+
*_, truncations, _ = env.step({agent: 1 for agent in env.agents})
|
|
16
|
+
|
|
17
|
+
assert all(truncations.values())
|
|
18
|
+
assert env.agents == []
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_unknown_environment_lists_available_ones():
|
|
22
|
+
with pytest.raises(KeyError, match="rps-v0"):
|
|
23
|
+
frondori_engine.make("chess-v0")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize("bad_id", ["rps", "Rps-v0", "rps-v", "rps_v0"])
|
|
27
|
+
def test_ids_must_be_versioned(bad_id):
|
|
28
|
+
with pytest.raises(ValueError, match="nom-vN"):
|
|
29
|
+
frondori_engine.register(bad_id, RockPaperScissorsEnv)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_an_id_cannot_be_registered_twice():
|
|
33
|
+
with pytest.raises(ValueError, match="déjà enregistré"):
|
|
34
|
+
frondori_engine.register("rps-v0", RockPaperScissorsEnv)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def fake_entry_points(*entries):
|
|
38
|
+
return lambda group: [EntryPoint(name, value, group) for name, value in entries if group == registry.ENTRY_POINT_GROUP]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_installed_environments_are_discovered(monkeypatch):
|
|
42
|
+
monkeypatch.setattr(registry, "_registry", {})
|
|
43
|
+
monkeypatch.setattr(registry, "_discovered", False)
|
|
44
|
+
monkeypatch.setattr(registry, "entry_points", fake_entry_points(("game-v0", "rps:RockPaperScissorsEnv")))
|
|
45
|
+
|
|
46
|
+
assert frondori_engine.registered_ids() == ["game-v0"]
|
|
47
|
+
env = frondori_engine.make("game-v0", rounds=2)
|
|
48
|
+
assert isinstance(env, RockPaperScissorsEnv) and env.rounds == 2
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_an_environment_class_is_only_imported_when_created(monkeypatch):
|
|
52
|
+
# Lister le catalogue ne doit charger aucun moteur.
|
|
53
|
+
monkeypatch.setattr(registry, "_registry", {})
|
|
54
|
+
monkeypatch.setattr(registry, "_discovered", False)
|
|
55
|
+
monkeypatch.setattr(registry, "entry_points", fake_entry_points(("broken-v0", "no_such_module:Env")))
|
|
56
|
+
|
|
57
|
+
assert frondori_engine.registered_ids() == ["broken-v0"]
|
|
58
|
+
with pytest.raises(ModuleNotFoundError):
|
|
59
|
+
frondori_engine.make("broken-v0")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_two_packages_declaring_the_same_id_is_an_error(monkeypatch):
|
|
63
|
+
monkeypatch.setattr(registry, "_registry", {})
|
|
64
|
+
monkeypatch.setattr(registry, "_discovered", False)
|
|
65
|
+
monkeypatch.setattr(
|
|
66
|
+
registry,
|
|
67
|
+
"entry_points",
|
|
68
|
+
fake_entry_points(("game-v0", "rps:RockPaperScissorsEnv"), ("game-v0", "rps:RockPaperScissorsEnv")),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
with pytest.raises(ValueError, match="déjà enregistré"):
|
|
72
|
+
frondori_engine.registered_ids()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Les vérifications du contrat, telles que les paquets d'environnement les
|
|
2
|
+
utilisent : elles passent sur un environnement conforme, et attrapent les
|
|
3
|
+
erreurs courantes avec un message utile."""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
from gymnasium import spaces
|
|
8
|
+
|
|
9
|
+
import frondori_engine
|
|
10
|
+
from frondori_engine.testing import (
|
|
11
|
+
check_environment,
|
|
12
|
+
check_metadata,
|
|
13
|
+
check_observations_fit_their_space,
|
|
14
|
+
check_pettingzoo_api,
|
|
15
|
+
)
|
|
16
|
+
from rps import RockPaperScissorsEnv
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_a_conforming_environment_passes_the_whole_contract():
|
|
20
|
+
check_environment("rps-v0")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SpacesRecreatedOnEveryCall(RockPaperScissorsEnv):
|
|
24
|
+
def action_space(self, agent):
|
|
25
|
+
return spaces.Discrete(4)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ObservationOutOfItsSpace(RockPaperScissorsEnv):
|
|
29
|
+
def _observations(self):
|
|
30
|
+
observations = super()._observations()
|
|
31
|
+
for observation in observations.values():
|
|
32
|
+
observation["rounds_left"] = 1000
|
|
33
|
+
return observations
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EloWithThreeAgents(RockPaperScissorsEnv):
|
|
37
|
+
def __init__(self, **kwargs):
|
|
38
|
+
super().__init__(**kwargs)
|
|
39
|
+
self.possible_agents = ["player_0", "player_1", "player_2"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NoBudget(RockPaperScissorsEnv):
|
|
43
|
+
metadata = {**RockPaperScissorsEnv.metadata, "compute_budget_ms": None}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize(
|
|
47
|
+
("broken", "check", "message"),
|
|
48
|
+
[
|
|
49
|
+
(SpacesRecreatedOnEveryCall, check_pettingzoo_api, "same space object"),
|
|
50
|
+
(ObservationOutOfItsSpace, check_observations_fit_their_space, "sort de son observation_space"),
|
|
51
|
+
(EloWithThreeAgents, check_metadata, "duel"),
|
|
52
|
+
(NoBudget, check_metadata, "compute_budget_ms"),
|
|
53
|
+
],
|
|
54
|
+
)
|
|
55
|
+
def test_common_mistakes_are_caught(broken, check, message):
|
|
56
|
+
frondori_engine.register("broken-v0", broken)
|
|
57
|
+
|
|
58
|
+
with pytest.raises(AssertionError, match=message):
|
|
59
|
+
check("broken-v0")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_the_neutral_action_is_the_zero_of_the_action_space():
|
|
63
|
+
from frondori_engine import wire
|
|
64
|
+
|
|
65
|
+
assert wire.neutral_action(spaces.Discrete(4)) == 0
|
|
66
|
+
box = wire.neutral_action(spaces.Box(-1, 1, shape=(2, 3), dtype=np.float32))
|
|
67
|
+
assert box.shape == (2, 3) and not box.any()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Le worker, testé comme le serveur l'utilise : un vrai sous-processus,
|
|
2
|
+
piloté en MessagePack par son entrée et sa sortie standard. Il joue
|
|
3
|
+
l'environnement d'exemple (`tests/rps.py`), enregistré avant de démarrer."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import struct
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import textwrap
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import msgpack
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
TESTS = Path(__file__).parent
|
|
16
|
+
|
|
17
|
+
BOOTSTRAP = textwrap.dedent(f"""
|
|
18
|
+
import sys
|
|
19
|
+
sys.path.insert(0, {str(TESTS)!r})
|
|
20
|
+
import frondori_engine
|
|
21
|
+
from rps import RockPaperScissorsEnv
|
|
22
|
+
frondori_engine.register("rps-v0", RockPaperScissorsEnv, package="frondori-rps")
|
|
23
|
+
{{extra}}
|
|
24
|
+
from frondori_engine.worker import main
|
|
25
|
+
main()
|
|
26
|
+
""")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Worker:
|
|
30
|
+
def __init__(self, extra: str = ""):
|
|
31
|
+
self._process = subprocess.Popen(
|
|
32
|
+
[sys.executable, "-c", BOOTSTRAP.replace("{extra}", textwrap.dedent(extra))],
|
|
33
|
+
stdin=subprocess.PIPE,
|
|
34
|
+
stdout=subprocess.PIPE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def request(self, message: dict) -> dict:
|
|
38
|
+
payload = msgpack.packb(message, use_bin_type=True)
|
|
39
|
+
self._process.stdin.write(struct.pack(">I", len(payload)) + payload)
|
|
40
|
+
self._process.stdin.flush()
|
|
41
|
+
(size,) = struct.unpack(">I", self._process.stdout.read(4))
|
|
42
|
+
return msgpack.unpackb(self._process.stdout.read(size), raw=False)
|
|
43
|
+
|
|
44
|
+
def close(self) -> None:
|
|
45
|
+
self._process.stdin.close()
|
|
46
|
+
assert self._process.wait(timeout=10) == 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def worker():
|
|
51
|
+
w = Worker()
|
|
52
|
+
yield w
|
|
53
|
+
w.close()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_describe_gives_what_the_server_and_the_website_need(worker):
|
|
57
|
+
reply = worker.request({"cmd": "describe"})
|
|
58
|
+
|
|
59
|
+
assert reply["ok"]
|
|
60
|
+
rps = reply["environments"]["rps-v0"]
|
|
61
|
+
assert rps["agents"] == ["player_0", "player_1"]
|
|
62
|
+
assert rps["tick_rate"] == 10.0
|
|
63
|
+
assert rps["ranking"] == "elo"
|
|
64
|
+
assert rps["compute_budget_ms"] == 50.0
|
|
65
|
+
assert rps["title"] == "Rock Paper Scissors"
|
|
66
|
+
assert rps["documentation"]
|
|
67
|
+
assert rps["package"] == "frondori-rps"
|
|
68
|
+
assert rps["action_spaces"]["player_0"] == {"type": "discrete", "n": 4, "start": 0}
|
|
69
|
+
assert rps["observation_spaces"]["player_0"]["type"] == "dict"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_an_episode_runs_through_the_worker(worker):
|
|
73
|
+
start = worker.request({"cmd": "start", "env_id": "rps-v0", "seed": 0})
|
|
74
|
+
|
|
75
|
+
assert start["ok"]
|
|
76
|
+
assert start["agents"] == ["player_0", "player_1"]
|
|
77
|
+
assert start["observations"]["player_0"] == {"opponent_last": 0, "rounds_left": 100}
|
|
78
|
+
json.loads(start["scene"])
|
|
79
|
+
|
|
80
|
+
# player_0 joue la pierre (1), player_1 n'a pas répondu à temps (nil).
|
|
81
|
+
step = worker.request({"cmd": "step", "actions": {"player_0": 1, "player_1": None}})
|
|
82
|
+
|
|
83
|
+
assert step["ok"]
|
|
84
|
+
assert step["rejected"] == []
|
|
85
|
+
assert step["rewards"] == {"player_0": 1.0, "player_1": -1.0}
|
|
86
|
+
assert step["observations"]["player_1"]["opponent_last"] == 1
|
|
87
|
+
assert step["infos"]["player_0"] == {"score": 1}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_invalid_actions_are_replaced_by_the_neutral_action(worker):
|
|
91
|
+
worker.request({"cmd": "start", "env_id": "rps-v0", "seed": 0})
|
|
92
|
+
|
|
93
|
+
step = worker.request({"cmd": "step", "actions": {"player_0": 42, "player_1": "n'importe quoi"}})
|
|
94
|
+
|
|
95
|
+
assert step["ok"]
|
|
96
|
+
assert sorted(step["rejected"]) == ["player_0", "player_1"]
|
|
97
|
+
# Les deux ont "passé" (action neutre 0) : manche nulle.
|
|
98
|
+
assert step["rewards"] == {"player_0": 0.0, "player_1": 0.0}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_an_error_is_reported_without_killing_the_worker(worker):
|
|
102
|
+
reply = worker.request({"cmd": "start", "env_id": "chess-v0", "seed": 0})
|
|
103
|
+
|
|
104
|
+
assert not reply["ok"]
|
|
105
|
+
assert "chess-v0" in reply["error"]
|
|
106
|
+
assert worker.request({"cmd": "describe"})["ok"]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_a_stray_print_in_an_environment_does_not_corrupt_the_protocol():
|
|
110
|
+
worker = Worker("""
|
|
111
|
+
class Noisy(RockPaperScissorsEnv):
|
|
112
|
+
def reset(self, seed=None, options=None):
|
|
113
|
+
print("un print oublié dans un environnement")
|
|
114
|
+
return super().reset(seed=seed, options=options)
|
|
115
|
+
|
|
116
|
+
frondori_engine.register("noisy-v0", Noisy)
|
|
117
|
+
""")
|
|
118
|
+
|
|
119
|
+
reply = worker.request({"cmd": "start", "env_id": "noisy-v0", "seed": 0})
|
|
120
|
+
|
|
121
|
+
assert reply["ok"]
|
|
122
|
+
worker.close()
|