Initial Commit

This commit is contained in:
oupson 2022-11-19 21:06:11 +01:00
commit 7f5f52e0eb
Signed by: oupson
GPG Key ID: 3BD88615552EFCB7
38 changed files with 980 additions and 0 deletions

71
README Executable file
View File

@ -0,0 +1,71 @@
Serveur Blackjack
Le but de ce DM est d'écrire un serveur pour un jeu simplifié de blackjack en suivant un protocole fourni. Un lab est proposé contenant une machine server sur lequel lancer votre application, ainsi que 6 machines pc1 à pc6 sur lesquelles vous pourrez faire tourner des clients. Deux applications clientes sont fournies ("shared/croupier.py") et ("shared/joueur.py").
Vous devrez programmer ce serveur en python en utilisant l'API asyncio vue en TP.
1. Le lab et le serveur
La machine server du lab doit héberger le serveur, qui sera lancé au démarrage (commande à décommenter dans "server.startup"). Le code du serveur est contenu dans un fichier "serverblackjack.py" placé dans le répertoire "shared" du lab. Au lancement, le serveur se positionne en écoute sur les ports 667 et 668. Un croupier ("shared/croupier.py"), exécuté depuis une des 6 autres machines, se connecte au serveur et l'échange permet au croupier de donner au serveur les paramètres de la table de jeu que le serveur va ensuite créer. La connexion se termine une fois les paramètres transmis. Un joueur ("shared/joueur.py"), exécuté depuis une des 6 autres machines, se connecte au serveur, rejoint une table et joue.
Le serveur doit pouvoir gérer un nombre arbitraire de tables en attente de joueurs ou en cours de jeu, et un nombre arbitraire de joueurs par table.
2. Le jeu
On joue avec un paquet de 52 cartes classiques : 4 couleurs, 13 cartes par couleur (cf https://fr.wikipedia.org/wiki/Jeu_de_cartes_fran%C3%A7ais).
Lorsqu'un joueur se connecte au serveur, il donne un nom de table auquel il souhaite se connecter. Cette table doit avoir été créée par un croupier auparavant. S'il est le premier joueur à cette table, une attente débute pendant un temps passé en paramètre lors de la création de la table. Pendant ce temps, d'autres joueurs peuvent se connecter à la même table. Une fois ce délai passé, il ne sera plus possible de rejoindre la table.
Le serveur prend alors un jeu de 52 cartes, qu'il mélange puis donne une carte à chaque joueur, prend une carte et recommence l'opération, de sorte que chaque joueur a 2 cartes ainsi que le donneur. Le but du jeu est de constituer un ensemble de cartes dont la valeur totale est la plus grande possible tout en restant inférieure ou égale à 21. Seule la première carte du donneur est connue des joueurs. Partant de ses 2 cartes, chaque joueur peut choisir de continuer à prendre des cartes une par une et de s'arrêter lorsqu'il le souhaite. S'il dépasse 21, il a perdu, s'il s'arrête avant, le donneur doit retourner sa seconde carte puis à son tour prendre des cartes une par une et s'arrêter lorsqu'il le souhaite. Si le donneur non plus n'a pas dépassé 21, le joueur gagne s'il a un meilleur total que le donneur, fait match nul en cas d'égalité et perd sinon. Dans une table à plusieurs joueurs, tous les joueurs jouent avant que le donneur ne retourne sa seconde carte et complète éventuellement sa main.
La stratégie du donneur est fixée : il s'arrête s'il a 17 ou plus et prend une carte sinon.
Les valeurs des cartes ne dépendent pas de la couleur :
- les as valent 1 ou 11, on choisit la valeur la plus favorable;
- les dix, valets, dames ou rois valent 10;
- les autres cartes valent leur chiffre, par exemple le sept de pique vaut 7.
Une fois la partie démarrée sur une table, pour chaque joueur successivement :
- le serveur envoie au joueur la liste de ses cartes, ainsi que la première carte du donneur;
- le serveur demande au joueur s'il souhaite une carte supplémentaire;
- le joueur demande une carte ou s'arrête;
- on répète ces 3 opérations jusqu'à ce que le joueur décide de s'arrêter.
Quand tous les joueurs ont joué, le donneur joue à son tour en prenant des cartes tant qu'il n'a pas atteint 17, puis il informe les joueurs du résultat.
3. Le protocole
On implémente un protocole textuel avec des messages de trois types :
1) "COMMAND" où "COMMAND" est une commande parmi {END, .};
2) "COMMAND value" où "COMMAND" est une commande parmi {NAME, TIME, MORE} et "value" est un paramètre (éventuellement vide) transmis sous forme de chaîne de caractères;
3) une chaîne de caractères quelconque.
Dans le cas du 3ème type, la syntaxe est libre. En revanche, dans le second cas, "value" est un paramètre de la commande envoyée par le client. Sauf précision dans le corps du sujet, les messages envoyés consistent en une unique ligne terminée par un "\n".
3.1 Le croupier comme client
Lorsqu'un croupier se connecte sur le port 668 du serveur, le serveur envoie un message de bienvenue. Le croupier répond avec la commande NAME suivie du nom de la table que le croupier veut créer. Le serveur confirme la réception et attend ensuite le délai (en secondes) que le croupier veut fixer entre la connexion d'un premier joueur et le début de la partie. Le croupier répond avec la commande TIME suivie du délai.
Le rôle du croupier s'arrête ici.
3.2 Le joueur comme client
Lorsqu'un joueur se connecte sur le port 667 du serveur, le serveur envoie un message de bienvenue. Le joueur répond avec la commande NAME suivie du nom de la table que le joueur veut rejoindre. Si aucune table ouverte ne correspond, le serveur termine l'échange avec la commande END. Sinon, il enregistre le joueur comme participant à cette table. Si c'est le premier joueur à cette table, l'attente d'autres joueurs commence et se terminera à la fin du délai passé en paramètre à la table. Une fois le délai passé, il n'est plus possible de se connecter et la partie commence. Le serveur distribue puis communique avec chaque joueur :
- le serveur donne au joueur la liste des cartes du joueur, le score correspondant ainsi que la première carte du donneur. Il attend ensuite de savoir si le joueur veut une carte supplémentaire. Cela peut se faire en plusieurs messages et se termine par la commande '.'
- le joueur répond par la commande MORE suivie de 1 pour une carte, 0 sinon.
- ces étapes sont répétées jusqu'à ce que le joueur s'arrête
- si le joueur a dépassé 21, le serveur lui donne le résultat.
Une fois que tous les joueurs ont joué, le serveur choisit lui même de prendre des cartes tant qu'il n'a pas atteint au moins 17. Il annonce ensuite le résultat à chaque joueur :
- le serveur envoie ses cartes et son score ainsi que le résultat puis termine avec la commande END
- le serveur ferme la connexion avec le joueur
Le serveur supprime finalement la table.
4. Attendus du serveur
- Le serveur doit pouvoir gérer toutes les demandes de connexion de manière concurrente, il doit être possible de créer plusieurs tables et plusieurs parties peuvent être en cours à un moment donné.
- Chaque table ne peut accueillir qu'une partie et doit être supprimée à la fin de celle-ci.
- Les tours de jeu des joueurs doivent se faire de manière concurrente sans ordre établi entre eux.
5. Travail demandé
Vous écrirez le code du serveur dans un fichier "serverblackjack.py". Il doit pouvoir interagir avec les clients proposés comme exemples (fichier "/shared/croupier.py" et "/shared/joueur.py" dans le lab).
6. Réalisation, retour et évaluation
Le travail est à effectuer en binôme ou individuellement. Vous écrirez un unique fichier "serverblackjack.py" que vous placerez dans une archive "prenom1-nom1.prenom2-nom2.tar.gz (en remplaçant prénoms et noms par ceux de votre binôme) à déposer au plus tard le 27 novembre 2022 à 23h59 dans le dépôt prévu à cet effet sur la page celene. Tous les binômes sont autorisés y compris entre Ingé et MIAGE, mais chaque binôme doit rendre un et un seul devoir.
Des pénalités seront appliquées en cas de retard, d'archive mal formée (il ne faut pas mettre le lab dans l'archive !) ou mal nommée...
L'évaluation sera réalisée en testant et en lisant votre code, le test aura lieu dans un lab similaire à celui qui vous est fourni.
Le but est de fournir une application fonctionnelle : en cas d'erreur ou de message d'erreur sur une fonctionnalité, il n'y aura pas de point au titre de l'esthétique, de l'originalité ou d'un code partiellement correct.

70
README~ Executable file
View File

@ -0,0 +1,70 @@
Serveur Blackjack
Le but de ce DM est d'écrire un serveur pour un jeu simplifié de blackjack en suivant un protocole fourni. Un lab est proposé contenant une machine server sur lequel lancer votre application, ainsi que 6 machines pc1 à pc6 sur lesquelles vous pourrez faire tourner des clients. Deux applications clientes sont fournies ("shared/croupier.py") et ("shared/joueur.py").
Vous devrez programmer ce serveur en python en utilisant l'API asyncio vue en TP.
1. Le lab et le serveur
La machine server du lab doit héberger le serveur, qui sera lancé au démarrage (commande à décommenter dans "server.startup"). Le code du serveur est contenu dans un fichier "serverblackjack.py" placé dans le répertoire "shared" du lab. Au lancement, le serveur se positionne en écoute sur les ports 667 et 668. Un croupier ("shared/croupier.py"), exécuté depuis une des 6 autres machines, se connecte au serveur et l'échange permet au croupier de donner au serveur les paramètres de la table de jeu que le serveur va ensuite créer. La connexion se termine une fois les paramètres transmis. Un joueur ("shared/joueur.py"), exécuté depuis une des 6 autres machines, se connecte au serveur, rejoint une table et joue.
Le serveur doit pouvoir gérer un nombre arbitraire de tables en attente de joueurs ou en cours de jeu.
2. Le jeu
On joue avec un paquet de 52 cartes classiques (4 couleurs, 13 cartes par couleur).
Lorsqu'un joueur se connecte au serveur, il donne un nom de table auquel il souhaite se connecter. S'il est le premier joueur à cette table, une attente débute pendant un temps passé en paramètre lors de la création de la table. Pendant ce temps, d'autres joueurs peuvent se connecter à la même table. Une fois ce délai passé, il ne sera plus possible de rejoindre la table.
Le serveur prend alors un jeu de 52 cartes, qu'il mélange puis donne une carte à chaque joueur, prend une carte et recommence l'opération, de sorte que chaque joueur a 2 cartes ainsi que le donneur. Le but du jeu est de constituer un ensemble de cartes dont la valeur totale est la plus grande possible tout en restant inférieure à 21. Partant de 2 cartes, le joueur peut choisir de continuer à prendre des cartes une par une et de s'arrêter lorsqu'il le souhaite. Il connaît la première carte du donneur uniquement. S'il dépasse 21, il a perdu, s'il s'arrête avant, le donneur doit retourner sa seconde carte puis à son tour prendre des cartes une par une et s'arrêter lorsqu'il le souhaite. Si personne n'a dépassé 21, le joueur gagne s'il a un meilleur total que le donneur, fait match nul en cas d'égalité et perd sinon. Dans une table à plusieurs joueurs, tous les joueurs jouent avant que le donneur ne retourne sa seconde carte et complète éventuellement sa main.
La stratégie du donneur est fixée : il s'arrête s'il a 17 ou plus et prend une carte sinon.
Les valeurs des cartes ne dépendent pas de la couleur :
- les as valent 1 ou 11, on choisit la valeur la plus favorable;
- les dix, valets, dames ou rois valent 10;
- les autres cartes valent leur chiffre, par exemple le sept de pique vaut 7.
Une fois la partie démarrée sur une table, pour chaque joueur successivement :
- le serveur envoie au joueur la liste de ses cartes, ainsi que la première carte du donneur;
- le serveur demande au joueur s'il souhaite une carte supplémentaire;
- le joueur demande une carte ou s'arrête;
- on répète ces 3 opérations jusqu'à ce que le joueur décide de s'arrêter.
Quand tous les joueurs ont joué, le donneur joue à son tour en prenant des cartes tant qu'il n'a pas atteint 17, puis il informe les joueurs du résultat.
3. Le protocole
On implémente un protocole textuel avec des messages de trois types :
1) "COMMAND" où "COMMAND" est une commande parmi {END, .};
2) "COMMAND value" où "COMMAND" est une commande parmi {NAME, TIME, MORE} et "value" est un paramètre (éventuellement vide) transmis sous forme de chaîne de caractères;
3) une chaîne de caractères quelconque.
Dans le cas du 3ème type, la syntaxe est libre. En revanche, "value" est un paramètre de la commande envoyée par le client. Sauf précision dans le corps du sujet, les messages envoyés consistent en une unique ligne terminée par un "\n".
3.1 Le croupier comme client
Lorsqu'un croupier se connecte sur le port 668 du serveur, le serveur envoie un message de bienvenue et demande le nom de la table à créer. Le croupier répond avec la commande NAME suivie du nom. Le serveur demande ensuite le délai à attendre entre la connexion d'un premier joueur et le début de la partie. Le croupier répond avec la commande TIME suivie du délai.
Le rôle du croupier s'arrête ici.
3.2 Le joueur comme client
Lorsqu'un joueur se connecte sur le port 667 du serveur, le serveur envoie un message de bienvenue et demande le nom de la table que le joueur veut rejoindre. Le joueur répond avec la commande NAME suivie du nom. Si aucune table ouverte ne correspond, le serveur termine l'échange avec la commande END. Sinon, il enregistre le joueur comme participant à cette table. Si c'est le premier joueur à cette table, l'attente d'autres joueurs commence et se termine à la fin du délai passé en paramètre à la table. Une fois le dali passé, il n'est plus possible de se connecter et la partie commence. Le serveur distribue puis échange avec chaque joueur :
- le serveur donne au joueur la liste des cartes du joueur, son score du moment ainsi que la première carte du donneur. Il demande ensuite au joueur si celui-ci veut une carte supplémentaire.Ccela peut se faire en plusieurs messages et se termine par la commande '.'
- le joueur répond par la commande MORE suivie de 1 pour une carte, 0 sinon.
- ces étapes sont répétées jusqu'à ce que le joueur s'arrête
- si le joueur a dépassé 21, le serveur lui donne le résultat.
Une fois que tous les joueurs ont joué, le serveur choisit lui même de prendre des cartes tant qu'il n'a pas atteint 17. Il annonce ensuite le résultat à chaque joueur :
- le serveur envoie ses cartes et son score ainsi que le résultat puis termine avec la commande END
- le serveur ferme la connexion avec le joueur
Le serveur supprime finalement la table.
4. Attendus du serveur
- Le serveur doit pouvoir gérer toutes les demandes de connexion de manière concurrente, il doit être possible de créer plusieurs tables et plusieurs parties peuvent être en cours à un moment donné.
- Chaque table ne peut accueillir qu'une partie et doit être supprimée à la fin de celle-ci.
- Les tours de jeu des joueurs doivent se faire de manière concurrente sans ordre établi.
5. Travail demandé
Vous écrirez le code du serveur dans un fichier "serverblackjack.py". Il doit pouvoir interagir avec les clients proposés comme exemples (fichier "/shared/croupier.py" et "/shared/joueur.py" dans le lab).
6. Réalisation, retour et évaluation
Le travail est à effectuer en binôme ou individuellement. Vous écrirez un unique fichier "serverblackjack.py" que vous placerez dans une archive "prenom1-nom1.prenom2-nom2.tar.gz (en remplaçant prénoms et noms par ceux de votre binôme) à déposer au plus tard le 20 novembre 2022 à 23h59 dans le dépôt prévu à cet effet pour votre groupe sur la page celene. Tous les binômes sont autorisés y compris entre Ingé et MIAGE, mais chaque binôme doit rendre un et un seul devoir dans l'un quelconque de vos dépôts si vous n'appartenez pas au même groupe..
Des pénalités seront appliquées en cas de retard, d'archive mal formée (il ne faut pas mettre le lab dans l'archive !) ou mal nommée...
L'évaluation sera réalisée en testant et en lisant votre code, le test aura lieu dans un lab similaire à celui qui vous est fourni.
Le but est de fournir une application fonctionnelle : en cas d'erreur ou de message d'erreur sur une fonctionnalité, il n'y aura pas de point au titre de l'esthétique, de l'originalité ou d'un code partiellement correct.

28
lab.conf Executable file
View File

@ -0,0 +1,28 @@
LAB_DESCRIPTION="Devoir de programmation avec l'API socket -- Kathará edition"
LAB_VERSION=1.0
LAB_AUTHOR="Martin Delacourt"
LAB_EMAIL=martin.delacourt@univ-orleans.fr
###### pc1, pc2, pc3, pc4, pc5, pc6 on a same network
pc1[0]="rezo"
pc1[image]="nopid/3ia:latest"
pc2[0]="rezo"
pc2[image]="nopid/3ia:latest"
pc3[0]="rezo"
pc3[image]="nopid/3ia:latest"
pc4[0]="rezo"
pc4[image]="nopid/3ia:latest"
pc5[0]="rezo"
pc5[image]="nopid/3ia:latest"
pc6[0]="rezo"
pc6[image]="nopid/3ia:latest"
###### server running without terminal
server[0]="rezo"
server[image]="nopid/3ia:latest"
server[num_terms]=0

6
lab.dep Executable file
View File

@ -0,0 +1,6 @@
pc1: server
pc2: server
pc3: server
pc4: server
pc5: server
pc6: server

1
pc1.startup Executable file
View File

@ -0,0 +1 @@
dhclient

1
pc2.startup Executable file
View File

@ -0,0 +1 @@
dhclient

1
pc3.startup Executable file
View File

@ -0,0 +1 @@
dhclient

1
pc4.startup Executable file
View File

@ -0,0 +1 @@
dhclient

1
pc5.startup Executable file
View File

@ -0,0 +1 @@
dhclient

1
pc6.startup Executable file
View File

@ -0,0 +1 @@
dhclient

6
server.startup Executable file
View File

@ -0,0 +1,6 @@
ip addr add 10.0.1.1/8 dev eth0
echo "10.0.1.1 server" >> /etc/hosts
/etc/init.d/bind start
/etc/init.d/isc-dhcp-server start
/etc/init.d/ssh start
#/shared/serverblackjack.py

6
server.startup~ Executable file
View File

@ -0,0 +1,6 @@
ip addr add 10.0.1.1/8 dev eth0
echo "10.0.1.1 server" >> /etc/hosts
/etc/init.d/bind start
/etc/init.d/isc-dhcp-server start
/etc/init.d/ssh start
#/shared/servershifumi.py

17
server/etc/bind/db.10 Executable file
View File

@ -0,0 +1,17 @@
$TTL 60000
@ IN SOA 10.in-addr.arpa. nobody.nowhere. (
1 ; serial
28 ; refresh
14 ; retry
3600000 ; expire
0 ; negative cache ttl
)
@ IN NS 1.1.0.10.in-addr.arpa.
1.1.0.10.in-addr.arpa. IN A 10.0.1.1
1.1.0 PTR dns.iiia.net.
1.1.0 PTR www.iiia.net.
1.0.0 PTR pc1.iiia.net.
2.0.0 PTR pc2.iiia.net.
3.0.0 PTR pc3.iiia.net.
4.0.0 PTR pc4.iiia.net.

24
server/etc/bind/db.root Executable file
View File

@ -0,0 +1,24 @@
$TTL 60000
@ IN SOA dns.iiia.net. root.dns.iiia.net. (
1 ; serial
28 ; refresh
14 ; retry
3600000 ; expire
60000 ; negative cache ttl
)
@ IN NS dns.iiia.net.
dns.iiia.net. IN A 10.0.1.1
serveur.iiia.net. IN A 10.0.1.1
server.iiia.net. IN A 10.0.1.1
dns.iiia.net. IN A 10.0.1.1
www.iiia.net. IN A 10.0.1.1
smtp.iiia.net. IN A 10.0.1.1
pc1.iiia.net. IN A 10.0.0.1
pc2.iiia.net. IN A 10.0.0.2
pc3.iiia.net. IN A 10.0.0.3
pc4.iiia.net. IN A 10.0.0.4
pc5.iiia.net. IN A 10.0.0.5
pc6.iiia.net. IN A 10.0.0.6
iiia.net. IN MX 1 smtp.iiia.net.

22
server/etc/bind/db.root~ Executable file
View File

@ -0,0 +1,22 @@
$TTL 60000
@ IN SOA dns.iiia.net. root.dns.iiia.net. (
1 ; serial
28 ; refresh
14 ; retry
3600000 ; expire
60000 ; negative cache ttl
)
@ IN NS dns.iiia.net.
dns.iiia.net. IN A 10.0.1.1
serveur.iiia.net. IN A 10.0.1.1
server.iiia.net. IN A 10.0.1.1
dns.iiia.net. IN A 10.0.1.1
www.iiia.net. IN A 10.0.1.1
smtp.iiia.net. IN A 10.0.1.1
pc1.iiia.net. IN A 10.0.0.1
pc2.iiia.net. IN A 10.0.0.2
pc3.iiia.net. IN A 10.0.0.3
pc4.iiia.net. IN A 10.0.0.4
iiia.net. IN MX 1 smtp.iiia.net.

43
server/etc/bind/named.conf Executable file
View File

@ -0,0 +1,43 @@
// This is the primary configuration file for the BIND DNS server named.
//
// Please read /usr/share/doc/bind9/README.Debian.gz for information on the
// structure of BIND configuration files in Debian, *BEFORE* you customize
// this configuration file.
//
// If you are just adding zones, please do that in /etc/bind/named.conf.local
include "/etc/bind/named.conf.options";
// prime the server with knowledge of the root servers
zone "." {
type master;
file "/etc/bind/db.root";
};
// be authoritative for the localhost forward and reverse zones, and for
// broadcast zones as per RFC 1912
zone "localhost" {
type master;
file "/etc/bind/db.local";
};
zone "127.in-addr.arpa" {
type master;
file "/etc/bind/db.127";
};
zone "0.in-addr.arpa" {
type master;
file "/etc/bind/db.0";
};
zone "255.in-addr.arpa" {
type master;
file "/etc/bind/db.255";
};
zone "10.in-addr.arpa" {
type master;
file "/etc/bind/db.10";
};

7
server/etc/dhcp/dhcpd.conf Executable file
View File

@ -0,0 +1,7 @@
option domain-name "iiia.net";
option domain-name-servers 10.0.1.1;
subnet 10.0.0.0 netmask 255.0.0.0 {
range 10.0.0.1 10.0.0.50;
}

13
server/etc/dhcp/dhcpd.conf~ Executable file
View File

@ -0,0 +1,13 @@
option domain-name "iiia.net";
option domain-name-servers 10.0.1.1;
subnet 10.0.0.0 netmask 255.0.0.0 {
range 10.0.0.1 10.0.0.50;
host pc1 { hardware ethernet 1e:c6:ad:0d:26:fc; fixed-address pc1.iiia.net; }
host pc2 { hardware ethernet fe:94:e6:de:3d:36; fixed-address pc2.iiia.net; }
host pc3 { hardware ethernet be:07:e1:9d:1c:85; fixed-address pc3.iiia.net; }
host pc4 { hardware ethernet 0a:3b:af:03:d5:c7; fixed-address pc4.iiia.net; }
}

1
shared.startup Executable file
View File

@ -0,0 +1 @@
bash /shared/mojo/mojo

8
shared/.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
shared/.idea/misc.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ComposerSettings">
<execution />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" project-jdk-name="Python 3.10" project-jdk-type="Python SDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
shared/.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/shared.iml" filepath="$PROJECT_DIR$/.idea/shared.iml" />
</modules>
</component>
</project>

9
shared/.idea/shared.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,3 @@
9
mojo/mojo,9/5/95f3485543394e27f59a77c00936786a521a06f2

71
shared/README.md Executable file
View File

@ -0,0 +1,71 @@
Serveur Blackjack
Le but de ce DM est d'écrire un serveur pour un jeu simplifié de blackjack en suivant un protocole fourni. Un lab est proposé contenant une machine server sur lequel lancer votre application, ainsi que 6 machines pc1 à pc6 sur lesquelles vous pourrez faire tourner des clients. Deux applications clientes sont fournies ("shared/croupier.py") et ("shared/joueur.py").
Vous devrez programmer ce serveur en python en utilisant l'API asyncio vue en TP.
1. Le lab et le serveur
La machine server du lab doit héberger le serveur, qui sera lancé au démarrage (commande à décommenter dans "server.startup"). Le code du serveur est contenu dans un fichier "serverblackjack.py" placé dans le répertoire "shared" du lab. Au lancement, le serveur se positionne en écoute sur les ports 667 et 668. Un croupier ("shared/croupier.py"), exécuté depuis une des 6 autres machines, se connecte au serveur et l'échange permet au croupier de donner au serveur les paramètres de la table de jeu que le serveur va ensuite créer. La connexion se termine une fois les paramètres transmis. Un joueur ("shared/joueur.py"), exécuté depuis une des 6 autres machines, se connecte au serveur, rejoint une table et joue.
Le serveur doit pouvoir gérer un nombre arbitraire de tables en attente de joueurs ou en cours de jeu, et un nombre arbitraire de joueurs par table.
2. Le jeu
On joue avec un paquet de 52 cartes classiques : 4 couleurs, 13 cartes par couleur (cf https://fr.wikipedia.org/wiki/Jeu_de_cartes_fran%C3%A7ais).
Lorsqu'un joueur se connecte au serveur, il donne un nom de table auquel il souhaite se connecter. Cette table doit avoir été créée par un croupier auparavant. S'il est le premier joueur à cette table, une attente débute pendant un temps passé en paramètre lors de la création de la table. Pendant ce temps, d'autres joueurs peuvent se connecter à la même table. Une fois ce délai passé, il ne sera plus possible de rejoindre la table.
Le serveur prend alors un jeu de 52 cartes, qu'il mélange puis donne une carte à chaque joueur, prend une carte et recommence l'opération, de sorte que chaque joueur a 2 cartes ainsi que le donneur. Le but du jeu est de constituer un ensemble de cartes dont la valeur totale est la plus grande possible tout en restant inférieure ou égale à 21. Seule la première carte du donneur est connue des joueurs. Partant de ses 2 cartes, chaque joueur peut choisir de continuer à prendre des cartes une par une et de s'arrêter lorsqu'il le souhaite. S'il dépasse 21, il a perdu, s'il s'arrête avant, le donneur doit retourner sa seconde carte puis à son tour prendre des cartes une par une et s'arrêter lorsqu'il le souhaite. Si le donneur non plus n'a pas dépassé 21, le joueur gagne s'il a un meilleur total que le donneur, fait match nul en cas d'égalité et perd sinon. Dans une table à plusieurs joueurs, tous les joueurs jouent avant que le donneur ne retourne sa seconde carte et complète éventuellement sa main.
La stratégie du donneur est fixée : il s'arrête s'il a 17 ou plus et prend une carte sinon.
Les valeurs des cartes ne dépendent pas de la couleur :
- les as valent 1 ou 11, on choisit la valeur la plus favorable;
- les dix, valets, dames ou rois valent 10;
- les autres cartes valent leur chiffre, par exemple le sept de pique vaut 7.
Une fois la partie démarrée sur une table, pour chaque joueur successivement :
- le serveur envoie au joueur la liste de ses cartes, ainsi que la première carte du donneur;
- le serveur demande au joueur s'il souhaite une carte supplémentaire;
- le joueur demande une carte ou s'arrête;
- on répète ces 3 opérations jusqu'à ce que le joueur décide de s'arrêter.
Quand tous les joueurs ont joué, le donneur joue à son tour en prenant des cartes tant qu'il n'a pas atteint 17, puis il informe les joueurs du résultat.
3. Le protocole
On implémente un protocole textuel avec des messages de trois types :
1) "COMMAND" où "COMMAND" est une commande parmi {END, .};
2) "COMMAND value" où "COMMAND" est une commande parmi {NAME, TIME, MORE} et "value" est un paramètre (éventuellement vide) transmis sous forme de chaîne de caractères;
3) une chaîne de caractères quelconque.
Dans le cas du 3ème type, la syntaxe est libre. En revanche, dans le second cas, "value" est un paramètre de la commande envoyée par le client. Sauf précision dans le corps du sujet, les messages envoyés consistent en une unique ligne terminée par un "\n".
3.1 Le croupier comme client
Lorsqu'un croupier se connecte sur le port 668 du serveur, le serveur envoie un message de bienvenue. Le croupier répond avec la commande NAME suivie du nom de la table que le croupier veut créer. Le serveur confirme la réception et attend ensuite le délai (en secondes) que le croupier veut fixer entre la connexion d'un premier joueur et le début de la partie. Le croupier répond avec la commande TIME suivie du délai.
Le rôle du croupier s'arrête ici.
3.2 Le joueur comme client
Lorsqu'un joueur se connecte sur le port 667 du serveur, le serveur envoie un message de bienvenue. Le joueur répond avec la commande NAME suivie du nom de la table que le joueur veut rejoindre. Si aucune table ouverte ne correspond, le serveur termine l'échange avec la commande END. Sinon, il enregistre le joueur comme participant à cette table. Si c'est le premier joueur à cette table, l'attente d'autres joueurs commence et se terminera à la fin du délai passé en paramètre à la table. Une fois le délai passé, il n'est plus possible de se connecter et la partie commence. Le serveur distribue puis communique avec chaque joueur :
- le serveur donne au joueur la liste des cartes du joueur, le score correspondant ainsi que la première carte du donneur. Il attend ensuite de savoir si le joueur veut une carte supplémentaire. Cela peut se faire en plusieurs messages et se termine par la commande '.'
- le joueur répond par la commande MORE suivie de 1 pour une carte, 0 sinon.
- ces étapes sont répétées jusqu'à ce que le joueur s'arrête
- si le joueur a dépassé 21, le serveur lui donne le résultat.
Une fois que tous les joueurs ont joué, le serveur choisit lui même de prendre des cartes tant qu'il n'a pas atteint au moins 17. Il annonce ensuite le résultat à chaque joueur :
- le serveur envoie ses cartes et son score ainsi que le résultat puis termine avec la commande END
- le serveur ferme la connexion avec le joueur
Le serveur supprime finalement la table.
4. Attendus du serveur
- Le serveur doit pouvoir gérer toutes les demandes de connexion de manière concurrente, il doit être possible de créer plusieurs tables et plusieurs parties peuvent être en cours à un moment donné.
- Chaque table ne peut accueillir qu'une partie et doit être supprimée à la fin de celle-ci.
- Les tours de jeu des joueurs doivent se faire de manière concurrente sans ordre établi entre eux.
5. Travail demandé
Vous écrirez le code du serveur dans un fichier "serverblackjack.py". Il doit pouvoir interagir avec les clients proposés comme exemples (fichier "/shared/croupier.py" et "/shared/joueur.py" dans le lab).
6. Réalisation, retour et évaluation
Le travail est à effectuer en binôme ou individuellement. Vous écrirez un unique fichier "serverblackjack.py" que vous placerez dans une archive "prenom1-nom1.prenom2-nom2.tar.gz (en remplaçant prénoms et noms par ceux de votre binôme) à déposer au plus tard le 27 novembre 2022 à 23h59 dans le dépôt prévu à cet effet sur la page celene. Tous les binômes sont autorisés y compris entre Ingé et MIAGE, mais chaque binôme doit rendre un et un seul devoir.
Des pénalités seront appliquées en cas de retard, d'archive mal formée (il ne faut pas mettre le lab dans l'archive !) ou mal nommée...
L'évaluation sera réalisée en testant et en lisant votre code, le test aura lieu dans un lab similaire à celui qui vous est fourni.
Le but est de fournir une application fonctionnelle : en cas d'erreur ou de message d'erreur sur une fonctionnalité, il n'y aura pas de point au titre de l'esthétique, de l'originalité ou d'un code partiellement correct.

Binary file not shown.

43
shared/croupier.py Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -
from sys import argv
from socket import gethostbyname
import asyncio
async def sndrcv(reader,writer,msg):
"""envoie le message [msg] au serveur et affiche la réponse"""
if msg is not None:
writer.write(msg.encode() + b"\r\n")
data = await reader.readline()
print(data.decode())
def getint(msg):
"""demande une valeur entière à l'utilisateur"""
while True:
try:
y = int(input(msg))
break
except ValueError:
print("Oups! Réessaie avec un nombre valide...")
return(y)
async def blackjack_croupier(server):
reader, writer = await asyncio.open_connection(server, 6668) # TODO 668
await sndrcv(reader,writer,None)
name = input("Nom de la table ?\n")
await sndrcv(reader,writer,"NAME "+name)
time = getint("Temps d'attente entre la connexion du premier joueur et le début de la partie ?\n")
await sndrcv(reader,writer,"TIME "+str(time))
if __name__ == '__main__':
if len(argv)!=2:
print("usage: {scriptname} server".format(scriptname= argv[0]))
exit(1)
sname=argv[1]
server=gethostbyname(sname)
print("connecting to :", sname, server)
asyncio.run(blackjack_croupier(server))

50
shared/joueur.py Executable file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -
from sys import argv
from socket import gethostbyname
import asyncio
async def sndrcv(reader,writer,msg):
"""envoie le message [msg] au serveur et affiche la réponse"""
if msg is not None:
writer.write(msg.encode() + b"\r\n")
data = await reader.readline()
print (data.decode().strip())
return data.decode().strip()
def getint(msg):
"""demande une valeur entière à l'utilisateur"""
while True:
try:
y = int(input(msg))
break
except ValueError:
print("Oups! Réessaie avec un nombre valide...")
return(y)
async def blackjack_client(server):
reader, writer = await asyncio.open_connection(server, 6667)
await sndrcv(reader,writer,None)
name = input("À quelle table voulez-vous vous connecter ?\n")
s = await sndrcv(reader,writer,"NAME "+name)
while s.strip() != "END":
if s == '.':
more = getint("Voulez-vous une carte supplémentaire ? 1 pour oui, 0 pour non.\n")
while more not in range(2):
print("Une valeur parmi 0 et 1 est attendue.\n")
more = getint("Voulez-vous une carte supplémentaire ? 1 pour oui, 0 pour non.\n")
s = await sndrcv(reader,writer,"MORE "+str(more))
else :
s = await sndrcv(reader,writer,None)
if __name__ == '__main__':
if len(argv)!=2:
print("usage: {scriptname} server".format(scriptname= argv[0]))
exit(1)
sname=argv[1]
server=gethostbyname(sname)
print("connecting to :", sname, server)
asyncio.run(blackjack_client(server))

12
shared/mojo/macaddr Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env python3
from hashlib import sha1
from sys import argv
m = sha1()
m.update(argv[1].encode())
m.update(b"-")
m.update(argv[2].encode())
d = m.digest()
addr = [ (d[i]+d[i+6]) % 256 for i in range(6) ]
addr[0] &= 0xfe
addr[0] |= 0x02
print(':'.join(map(lambda x : ("00"+hex(x)[2:])[-2:],addr)))

6
shared/mojo/mojo Executable file
View File

@ -0,0 +1,6 @@
umount /etc/resolv.conf
umount /etc/hosts
echo '127.0.0.1 localhost' > /etc/hosts
for eth in $(ip -br a | grep '^eth' | cut -d@ -f1); do
ip link set $eth address $(/shared/mojo/macaddr $HOSTNAME $eth)
done

124
shared/server/__init__.py Normal file
View File

@ -0,0 +1,124 @@
import asyncio
import logging
import socket
from .game import ConcurrentData, TableAlreadyExistException
from .message import parse_message, BiCommand
class Server:
def __init__(self) -> None:
self.__data = ConcurrentData()
async def handle_client(self, client: socket.socket) -> None:
loop = asyncio.get_event_loop()
logging.info("New client connected")
await loop.sock_sendall(client, b"HELLO\r\n")
try:
request = parse_message((await loop.sock_recv(client, 4096)).decode('utf8'))
except BrokenPipeError:
return
if len(request) != 2:
await loop.sock_sendall(client, b"ERROR: INVALID COMMAND\r\nEND\r\n")
client.close()
return
if request[0] == BiCommand.NAME:
name = request[1]
else:
await loop.sock_sendall(client, b"ERROR: INVALID COMMAND\r\nEND\r\n")
client.close()
return
if not await self.__data.has_table(name):
await loop.sock_sendall(client, b"ERROR: NO SUCH TABLE\r\nEND\r\n")
client.close()
return
table = await self.__data.get_table(name)
if await table.add_client(client, self.__data):
await loop.sock_sendall(client, b"OK\r\n")
else:
await loop.sock_sendall(client, b"ERROR: ROOM FULL\r\nEND\r\n")
client.close()
async def handle_croupier(self, client: socket.socket) -> None:
logging.debug("New croupier connected")
loop = asyncio.get_event_loop()
await loop.sock_sendall(client, b"HELLO\r\n")
try:
request = parse_message((await loop.sock_recv(client, 4096)).decode('utf8'))
except BrokenPipeError:
return
if len(request) != 2:
await loop.sock_sendall(client, b"ERROR: INVALID COMMAND\r\nEND\r\n")
client.close()
return
if request[0] == BiCommand.NAME:
name = request[1]
await loop.sock_sendall(client, b"OK\r\n")
else:
await loop.sock_sendall(client, b"ERROR: INVALID COMMAND\r\nEND\r\n")
client.close()
return
try:
request = parse_message((await loop.sock_recv(client, 4096)).decode('utf8'))
except BrokenPipeError:
return
if len(request) != 2:
await loop.sock_sendall(client, b"ERROR: INVALID COMMAND\r\nEND\r\n")
client.close()
return
if request[0] == BiCommand.TIME:
time = request[1]
try:
await self.__data.add_table(name, int(time))
await loop.sock_sendall(client, b"OK\r\n")
except TableAlreadyExistException:
await loop.sock_sendall(client, b"ERROR: TABLE ALREADY EXIST\r\nEND\r\n")
else:
await loop.sock_sendall(client, b"ERROR: INVALID COMMAND\r\nEND\r\n")
client.close()
async def listen_client(self) -> None:
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.bind(('localhost', 667))
listen_socket.listen(8)
listen_socket.setblocking(False)
loop = asyncio.get_event_loop()
while True:
client, _ = await loop.sock_accept(listen_socket)
loop.create_task(self.handle_client(client))
async def listen_croupier(self) -> None:
server_croupier = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_croupier.bind(('localhost', 668))
server_croupier.listen(8)
server_croupier.setblocking(False)
loop = asyncio.get_event_loop()
while True:
client, _ = await loop.sock_accept(server_croupier)
loop.create_task(self.handle_croupier(client))
async def run(self):
loop = asyncio.get_event_loop()
loop_client = loop.create_task(self.listen_client())
loop_croupier = loop.create_task(self.listen_croupier())
logging.info("Running server ...")
await asyncio.gather(loop_client, loop_croupier)

Binary file not shown.

Binary file not shown.

Binary file not shown.

250
shared/server/game.py Normal file
View File

@ -0,0 +1,250 @@
import asyncio
import logging
import random
import socket
from enum import Enum
from typing import List, Dict, Tuple, Optional
from .message import parse_message, BiCommand
class Color(Enum):
TREFLE = "trefle"
CARRAUX = "carraux"
COEUR = "cœurs"
PIQUES = "piques"
class Carte:
color: Color
value: int
def __init__(self, color: Color, value: int):
self.color = color
self.value = value
def __str__(self):
return str(self.color.value) + " " + str(self.value)
def create_deck() -> List[Carte]:
cartes = []
for color in [Color.TREFLE, Color.CARRAUX, Color.COEUR, Color.PIQUES]:
for num in range(1, 14):
cartes.append(Carte(color, num))
random.shuffle(cartes)
return cartes
class Table:
__name: str
__delay: int
def __init__(self, name: str, delay: int) -> None:
self.__name = name
self.__delay = delay
@property
def delay(self) -> int:
return self.__delay
@property
def name(self) -> str:
return self.__name
class Joueur:
__socket: socket.socket
cartes: List[Carte]
def __init__(self, client_socket: socket.socket):
self.__socket = client_socket
self.cartes = list()
@property
def socket(self) -> socket.socket:
return self.__socket
@property
def points(self) -> int:
return points_cartes(self.cartes)
def points_cartes(cartes: List[Carte]) -> int:
value = 0
as_count = 0
for carte in cartes:
if carte.value == 1:
as_count += 1
else:
value += carte.value
if as_count > 0:
if value + 11 + as_count - 1 < 21:
value += 11 + as_count - 1
else:
value += as_count
return value
class Game:
__players: List[Joueur]
__table: 'Table'
__started: bool
__accepting: bool
__cards: List[Carte]
__dealer_cards: List[Carte]
__logger: logging.Logger
def __init__(self, table: 'Table') -> None:
self.__table = table
self.__started = False
self.__players = []
self.__accepting = True
self.__cards = create_deck()
self.__dealer_cards = list()
self.__logger = logging.getLogger(f"Game-{table.name}")
async def add_client(self, client: socket.socket, data: 'ConcurrentData') -> bool:
if self.__accepting:
loop = asyncio.get_event_loop()
joueur = Joueur(client)
self.__players.append(joueur)
if not self.__started:
loop.create_task(self.run_game(data))
return True
else:
return False
async def run_game(self, data: 'ConcurrentData'):
self.__logger.error(f"Waiting {self.__table.delay}s")
self.__started = True
await asyncio.sleep(self.__table.delay)
self.__accepting = False
for _ in range(0, 2):
self.__dealer_cards.append(self.__cards.pop(0))
for joueur in self.__players:
joueur.cartes.append(self.__cards.pop(0))
loop = asyncio.get_event_loop()
for c in self.__players:
await loop.sock_sendall(c.socket, (
"Vos cartes sont : " + ", ".join(str(carte) for carte in c.cartes) + "\r\n").encode("utf8"))
await loop.sock_sendall(c.socket, (
"La premiere carte du donneur est : " + str(self.__dealer_cards[0]) + "\r\n").encode('utf8'))
await self.run_game_loop(loop)
for c in self.__players:
await loop.sock_sendall(c.socket, b"Service termine\r\n")
await loop.sock_sendall(c.socket,
f"L'autre carte du donneur est : {self.__dealer_cards[1]}\r\n".encode(
'utf8'))
await loop.sock_sendall(c.socket,
f"Vous avez {c.points} points\r\n".encode('utf8'))
max_player, max_player_index = self.max_player()
self.__logger.debug(f"Max player have {max_player} points")
while max_player is not None and len(self.__cards) > 0 and points_cartes(self.__dealer_cards) < max_player:
carte = self.__cards.pop(0)
for c in self.__players:
await loop.sock_sendall(c.socket, f"Le donneur a pioché : {carte}\r\n".encode('utf8'))
self.__dealer_cards.append(carte)
await self.send_victory_status(loop, max_player, max_player_index)
await data.remove_table(self.__table.name)
self.__logger.debug(f"Removed table \"{self.__table.name}\"")
async def run_game_loop(self, loop):
for i, joueur in enumerate(self.__players):
for j in self.__players:
await loop.sock_sendall(j.socket, f"C'est au tour de joueur {i + 1}\r\n".encode('utf8'))
playing = True
while playing:
if len(self.__cards) > 0 and joueur.points < 21:
await loop.sock_sendall(joueur.socket, b".\r\n")
request = parse_message((await loop.sock_recv(joueur.socket, 4096)).decode('utf8'))
if len(request) == 2 and request[0] == BiCommand.MORE:
if request[1] == "0":
playing = False
elif request[1] == "1":
carte = self.__cards.pop(0)
joueur.cartes.append(carte)
await loop.sock_sendall(joueur.socket,
f"Votre carte est : {carte}, il reste {len(self.__cards)} cartes\r\n".encode(
'utf8'))
else:
await loop.sock_sendall(joueur.socket, b"ERROR : INVALID COMMAND\r\n")
else:
await loop.sock_sendall(joueur.socket, b"ERROR : INVALID COMMAND\r\n")
else:
playing = False
async def send_victory_status(self, loop, max_player, max_player_index: List[int]):
points_donneur = points_cartes(self.__dealer_cards)
self.__logger.debug(f"Dealer have {points_donneur} points")
if (-1 if max_player is None else max_player) < points_donneur < 21:
for c in self.__players:
await loop.sock_sendall(c.socket, b"Vous avez perdu !\r\nEND\r\n")
c.socket.close()
else:
for i, c in enumerate(self.__players):
if i in max_player_index:
if max_player == points_donneur or len(max_player_index) > 1:
await loop.sock_sendall(c.socket, b"Egalite !\r\nEND\r\n")
else:
await loop.sock_sendall(c.socket, b"Vous avez gagne !\r\nEND\r\n")
else:
await loop.sock_sendall(c.socket, b"Vous avez perdu !\r\nEND\r\n")
c.socket.close()
def max_player(self) -> Tuple[Optional[int], Optional[List[int]]]:
m = None
mp = []
for i, j in enumerate(self.__players):
p = j.points
if p < 21:
if m is None or m < p:
m = p
mp = [i]
elif m == p:
mp.append(i)
return m, mp
class TableAlreadyExistException(Exception):
pass
class ConcurrentData:
def __init__(self) -> None:
self.__lock = asyncio.Lock()
self.__data: Dict[str, Game] = dict()
async def add_table(self, table_name: str, delay: int):
async with self.__lock:
if table_name in self.__data.keys():
raise TableAlreadyExistException()
else:
self.__data[table_name] = Game(Table(table_name, delay))
logging.debug("Created a new table called %s" % table_name)
async def has_table(self, table_name: str) -> bool:
async with self.__lock:
return table_name in self.__data.keys()
async def get_table(self, table_name: str) -> Game:
async with self.__lock:
return self.__data[table_name]
async def remove_table(self, table_name: str):
async with self.__lock:
self.__data.pop(table_name)

54
shared/server/message.py Normal file
View File

@ -0,0 +1,54 @@
import logging
import re
from enum import Enum
from typing import Union, Tuple, AnyStr, Any
regex_command = r"^(?:(END|\.)|(?:(NAME|TIME|MORE) (\w+))|(?:(.+)\n))\r\n$"
class UniCommand(Enum):
END = 1
POINT = 2
@staticmethod
def from_str(command: str) -> 'UniCommand':
if command == "END":
return UniCommand.END
elif command == ".":
return UniCommand.POINT
else:
# TODO
pass
class BiCommand(Enum):
NAME = 1
TIME = 2
MORE = 3
@staticmethod
def from_str(command: str) -> 'BiCommand':
if command == "NAME":
return BiCommand.NAME
elif command == "TIME":
return BiCommand.TIME
elif command == "MORE":
return BiCommand.MORE
else:
# TODO
pass
def parse_message(message: str) -> Union[UniCommand, Tuple[BiCommand, str], Tuple[str, str]]:
matches = re.match(regex_command, message)
if matches:
groups: tuple[AnyStr | Any, ...] = matches.groups()
if groups[0] is not None:
return UniCommand.from_str(groups[0])
elif groups[1] is not None and groups[2] is not None:
return BiCommand.from_str(groups[1]), groups[2]
elif groups[3] is not None:
return groups[3]
# TODO Exception
pass

13
shared/serverblackjack.py Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -
import asyncio
import logging
import os
from server import Server
if __name__ == "__main__":
logging.basicConfig(level=os.environ.get("LOGLEVEL", "DEBUG"))
server = Server()
asyncio.run(server.run())