Import du code

This commit is contained in:
JonathanMM 2022-01-10 19:34:58 +01:00
parent f6eb48d0a5
commit 96981bbccc
29 changed files with 134615 additions and 1 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules/
js/
public/mots.txt
public/motsATrouve.txt
public/motsATrouveNettoyes.txt
public/motsNettoyes.txt
ts/mots/motsATrouver.prod.ts

View File

@ -1,3 +1,30 @@
# SUTOM
Jeu en ligne (et en français) basé sur Wordle
Jeu de lettres en ligne (et en français) basé sur Wordle. Le jeu se trouve à l'adresse https://sutom.nocle.fr
## Contributions
Tout d'abord, merci si vous contribuer :) Pour l'instant, le mieux, c'est de créer un ticket quand vous voyez un bug, ça me permettra de trier et de prioriser tout ce que je dois faire. Comme la base de code n'est pas aussi propre que je voudrais, merci de créer un ticket et d'attendre un retour de ma part ( @JonathanMM ) avant de vous lancer à corps perdu dans le code.
## Développement
Pour pouvoir travailler en local, il faut commencer par installer ce qu'il faut à node :
```sh
npm i
```
Puis, on lance le serveur :
```sh
npm start
```
Une fois démarré, le site sera dispo sur http://localhost:4000 et le typescript va se recompiler tout seul à chaque modification de fichier.
## Autres infos et remerciements
- Le dictionnaire vient d'ici : https://chrplr.github.io/openlexicon/datasets-info/Liste-de-mots-francais-Gutenberg/README-liste-francais-Gutenberg.html
- Merci à Emmanuel pour m'avoir fourni des mots à trouver
- Merci à tous les gens qui me remontent des bugs et qui me donnent des idées, ça m'aide beaucoup :)
- Merci à toutes les personnes qui jouent, c'est une belle récompense que vous me donnez.

1035
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "sutom",
"version": "1.0.0",
"description": "Jeu de lettres en ligne (et en français)",
"main": "js/main.js",
"dependencies": {
"@types/express": "^4.17.13",
"express": "^4.17.2",
"readline-sync": "^1.4.10",
"requirejs": "^2.3.6",
"typescript": "^4.5.4"
},
"devDependencies": {
"ts-node-dev": "^1.1.8",
"tsc-watch": "^4.6.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "tsc && node js/server.js & tsc-watch"
},
"keywords": [
"wordle",
"letters",
"word",
"word game"
],
"author": "JonathanMM <jonathanmm@free.fr>",
"repository": "https://framagit.org/JonathanMM/sutom",
"license": "MIT"
}

Binary file not shown.

7
public/htaccess.server Normal file
View File

@ -0,0 +1,7 @@
AddDefaultCharset UTF-8
#Force HTTPS
RewriteEngine On
RewriteCond %{SERVER_PORT} 80
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

89
public/index.html Normal file
View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SUTOM</title>
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
<script data-main="js/main" type="text/javascript" src="node_modules/requirejs/require.js"></script>
<link rel="stylesheet" href="jeu.css" type="text/css" />
</head>
<body>
<div id="contenu">
<h1>SUTOM</h1>
<div id="notification"> </div>
<div id="fin-de-partie-panel">
<div id="victoire-panel">
<h2>Félicitations</h2>
<p>Bravo, tu as gagné. Reviens demain pour une nouvelle grille.</p>
</div>
<div id="defaite-panel">
<h2>Perdu</h2>
<p>
Le mot a trouver était : <span id="defaite-panel-mot"></span><br />
Peut être feras-tu mieux demain ?
</p>
</div>
<p>Résumé de ta partie <a href="#" id="fin-de-partie-panel-resume-bouton">Partager</a></p>
<pre id="fin-de-partie-panel-resume"></pre>
</div>
<div id="grille"></div>
<div id="input-area">
<div class="input-ligne">
<div data-lettre="A" class="input-lettre">A</div>
<div data-lettre="Z" class="input-lettre">Z</div>
<div data-lettre="E" class="input-lettre">E</div>
<div data-lettre="R" class="input-lettre">R</div>
<div data-lettre="T" class="input-lettre">T</div>
<div data-lettre="Y" class="input-lettre">Y</div>
<div data-lettre="U" class="input-lettre">U</div>
<div data-lettre="I" class="input-lettre">I</div>
<div data-lettre="O" class="input-lettre">O</div>
<div data-lettre="P" class="input-lettre">P</div>
</div>
<div class="input-ligne">
<div data-lettre="Q" class="input-lettre">Q</div>
<div data-lettre="S" class="input-lettre">S</div>
<div data-lettre="D" class="input-lettre">D</div>
<div data-lettre="F" class="input-lettre">F</div>
<div data-lettre="G" class="input-lettre">G</div>
<div data-lettre="H" class="input-lettre">H</div>
<div data-lettre="J" class="input-lettre">J</div>
<div data-lettre="K" class="input-lettre">K</div>
<div data-lettre="L" class="input-lettre">L</div>
<div data-lettre="M" class="input-lettre">M</div>
</div>
<div class="input-ligne">
<div data-lettre="W" class="input-lettre">W</div>
<div data-lettre="X" class="input-lettre">X</div>
<div data-lettre="C" class="input-lettre">C</div>
<div data-lettre="V" class="input-lettre">V</div>
<div data-lettre="B" class="input-lettre">B</div>
<div data-lettre="N" class="input-lettre">N</div>
<div data-lettre="_effacer" class="input-lettre"></div>
<div data-lettre="_entree" class="input-lettre"></div>
</div>
</div>
<div id="regles-panel">
<p>
Vous avez six essais pour deviner le mot du jour.<br />
Vous ne pouvez proposer que des mots commençant par la même lettre que le mot recherché, et qui se trouvent dans notre dictionnaire.<br />
Les lettres entourées d'un carré rouge sont bien placées,<br />
les lettres entourées d'un cercle jaune sont mal placées (mais présentes dans le mot).<br />
Les lettres qui restent sur fond bleu ne sont pas dans le mot.<br />
Il y a un mot par jour, et il est identique pour tout le monde. Évitez donc les spoils et privilégiez le bouton de partage.<br />
En cas de soucis, vous pouvez contacter <a href="https://twitter.com/Jonamaths">@Jonamaths</a> sur twitter.
<a target="_blank" href="https://framagit.org/JonathanMM/sutom">Page du projet</a><br />
Basé sur l'excellent <a target="_blank" href="https://www.powerlanguage.co.uk/wordle/">Wordle</a> et le regretté Motus.<br />
Merci à Emmanuel pour l'aide sur le dictionnaire.
</p>
</div>
</div>
<script type="text/javascript">
requirejs(["main"], function (Main) {
var socket = new Main.default();
});
</script>
</body>
</html>

150
public/jeu.css Normal file
View File

@ -0,0 +1,150 @@
:root {
--taille-cellule: 48px;
--epaisseur-bordure-cellule: 1px;
--epaisseur-padding-cellule: 2px;
--couleur-bien-place: #e7002a;
--couleur-mal-place: #ffbd00;
--couleur-fond-grille: #0077c7;
--couleur-non-trouve: rgb(112, 112, 112);
}
@font-face {
font-family: "Roboto Medium";
src: url("/fonts/Roboto-Medium.ttf");
}
body {
font-family: "Roboto Medium", Ubuntu, Arial, Helvetica, sans-serif;
font-size: 32px;
background-color: #2b2b2b;
height: 100vh;
text-align: center;
color: white;
margin: 0;
padding: 0;
}
#contenu {
display: flex;
flex-direction: column;
margin-left: 25%;
margin-right: 25%;
justify-content: space-between;
height: 100%;
}
@media (max-width: 1024px) {
#contenu {
margin-left: 2px;
margin-right: 2px;
}
}
#grille {
margin-left: auto;
margin-right: auto;
background-color: var(--couleur-fond-grille);
}
#grille table {
border-spacing: 0;
}
#grille td {
width: calc(var(--taille-cellule) - 2 * var(--epaisseur-padding-cellule));
height: calc(var(--taille-cellule) - 2 * var(--epaisseur-padding-cellule));
text-align: center;
position: relative;
padding: var(--epaisseur-padding-cellule);
color: white;
border: 1px solid white;
z-index: 0;
}
#grille td:not(.resultat) {
background-color: #0077c7;
}
#grille td.resultat::after {
width: calc(var(--taille-cellule));
height: calc(var(--taille-cellule));
position: absolute;
top: 0;
left: 0;
z-index: -1;
content: " ";
}
#grille td.mal-place::after {
background-color: var(--couleur-mal-place);
border-radius: 50%;
}
#grille td.bien-place::after {
background-color: var(--couleur-bien-place);
}
#grille td.non-trouve::after {
background-color: var(--couleur-fond-grille);
}
#fin-de-partie-panel,
#victoire-panel,
#defaite-panel {
display: none;
font-size: 24px;
}
#input-area {
margin-top: 0.5em;
margin-bottom: 2em;
}
.input-ligne + .input-ligne {
margin-top: 0.5em;
}
.input-lettre {
font-size: 18px;
display: inline-block;
border: 1px solid white;
padding: 0.5em;
user-select: none;
min-width: 0.5em;
}
.input-lettre.lettre-bien-place {
background: var(--couleur-bien-place);
}
.input-lettre.lettre-mal-place {
background: var(--couleur-mal-place);
}
.input-lettre.lettre-non-trouve {
color: var(--couleur-non-trouve);
border: 1px solid var(--couleur-non-trouve);
}
.input-lettre:hover,
.input-lettre:active {
cursor: pointer;
}
#regles-panel {
font-size: 14px;
text-align: left;
}
#regles-panel a,
#regles-panel a:visited,
#fin-de-partie-panel a,
#fin-de-partie-panel a:visited {
color: white;
}
#notification {
opacity: 0;
transition: opacity linear 1s;
}

26
ts/dictionnaire.ts Normal file
View File

@ -0,0 +1,26 @@
import ListeMotsProposables from "./mots/listeMotsProposables";
import MotsATrouver from "./mots/motsATrouver";
export default class Dictionnaire {
public constructor() {}
public getMot(): string {
let aujourdhui = new Date().getTime();
let origine = new Date(2022, 0, 8).getTime();
let numeroGrille = Math.floor((aujourdhui - origine) / (24 * 3600 * 1000));
return MotsATrouver.Liste[numeroGrille];
}
public estMotValide(mot: string): boolean {
mot = this.nettoyerMot(mot);
return mot.length >= 6 && mot.length <= 9 && ListeMotsProposables.Dictionnaire.includes(mot);
}
public nettoyerMot(mot: string): string {
return mot
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toUpperCase();
}
}

71
ts/finDePartiePanel.ts Normal file
View File

@ -0,0 +1,71 @@
import LettreResultat from "./lettreResultat";
import { LettreStatut } from "./lettreStatut";
import NotificationMessage from "./notificationMessage";
export default class FinDePartiePanel {
private readonly _reglesPanel: HTMLElement;
private readonly _finDePartiePanel: HTMLElement;
private readonly _victoirePanel: HTMLElement;
private readonly _defaitePanel: HTMLElement;
private readonly _defaitePanelMot: HTMLElement;
private readonly _resume: HTMLPreElement;
private readonly _resumeBouton: HTMLElement;
private _resumeTexte: string = "";
public constructor() {
this._reglesPanel = document.getElementById("regles-panel") as HTMLElement;
this._finDePartiePanel = document.getElementById("fin-de-partie-panel") as HTMLElement;
this._victoirePanel = document.getElementById("victoire-panel") as HTMLElement;
this._defaitePanel = document.getElementById("defaite-panel") as HTMLElement;
this._defaitePanelMot = document.getElementById("defaite-panel-mot") as HTMLElement;
this._resume = document.getElementById("fin-de-partie-panel-resume") as HTMLPreElement;
this._resumeBouton = document.getElementById("fin-de-partie-panel-resume-bouton") as HTMLElement;
this._resumeBouton.addEventListener("click", (event) => {
event.stopPropagation();
if (!navigator.clipboard) {
NotificationMessage.ajouterNotification("Votre navigateur n'est pas compatible");
}
navigator.clipboard.writeText(this._resumeTexte + "\n\nhttps://sutom.nocle.fr");
NotificationMessage.ajouterNotification("Résumé copié dans le presse papier");
});
}
public genererResume(aBonneReponse: boolean, resultats: Array<Array<LettreResultat>>): void {
let resultatsEmojis = resultats.map((mot) =>
mot
.map((resultat) => resultat.statut)
.reduce((ligne, statut) => {
switch (statut) {
case LettreStatut.BienPlace:
return ligne + "🟥";
case LettreStatut.MalPlace:
return ligne + "🟡";
default:
return ligne + "🟦";
}
}, "")
);
let aujourdhui = new Date().getTime();
let origine = new Date(2022, 0, 8).getTime();
let numeroGrille = Math.floor((aujourdhui - origine) / (24 * 3600 * 1000)) + 1;
this._resumeTexte = "SUTOM #" + numeroGrille + " " + (aBonneReponse ? resultats.length : "-") + "/6\n\n" + resultatsEmojis.join("\n");
this._resume.innerText = this._resumeTexte;
}
public afficher(estVictoire: boolean, motATrouver: string): void {
this._reglesPanel.style.display = "none";
this._finDePartiePanel.style.display = "block";
if (estVictoire) this._victoirePanel.style.display = "block";
else {
this._defaitePanelMot.innerText = motATrouver;
this._defaitePanel.style.display = "block";
}
}
}

148
ts/gestionnaire.ts Normal file
View File

@ -0,0 +1,148 @@
import Dictionnaire from "./dictionnaire";
import Grille from "./grille";
import Input from "./input";
import LettreResultat from "./lettreResultat";
import { LettreStatut } from "./lettreStatut";
import FinDePartiePanel from "./finDePartiePanel";
import NotificationMessage from "./notificationMessage";
import SauvegardeStats from "./sauvegardeStats";
import Sauvegardeur from "./sauvegardeur";
export default class Gestionnaire {
private readonly _dictionnaire: Dictionnaire;
private readonly _grille: Grille;
private readonly _input: Input;
private readonly _sauvegardeur: Sauvegardeur;
private readonly _victoirePanel: FinDePartiePanel;
private readonly _propositions: Array<string>;
private readonly _resultats: Array<Array<LettreResultat>>;
private _motATrouver: string;
private _compositionMotATrouver: { [lettre: string]: number };
private _maxNbPropositions: number = 6;
private _datePartieEnCours: Date | undefined;
private _stats: SauvegardeStats = { partiesJouees: 0, partiesGagnees: 0 };
public constructor() {
this._dictionnaire = new Dictionnaire();
this._motATrouver = this.choisirMot();
this._grille = new Grille(this._motATrouver.length, this._maxNbPropositions, this._motATrouver[0]);
this._input = new Input(this, this._motATrouver.length);
this._sauvegardeur = new Sauvegardeur();
this._victoirePanel = new FinDePartiePanel();
this._propositions = new Array<string>();
this._resultats = new Array<Array<LettreResultat>>();
this._compositionMotATrouver = this.decompose(this._motATrouver);
this.chargerSauvegarde();
}
private chargerSauvegarde(): void {
let sauvegardePartieEnCours = this._sauvegardeur.chargerSauvegardePartieEnCours();
if (sauvegardePartieEnCours) {
this._datePartieEnCours = sauvegardePartieEnCours.datePartie;
for (let mot of sauvegardePartieEnCours.propositions) {
this.verifierMot(mot, true);
}
}
this._stats = this._sauvegardeur.chargerSauvegardeStats() ?? { partiesJouees: 0, partiesGagnees: 0 };
}
private enregistrerPartieDansStats(): void {
this._stats.partiesJouees++;
if (this._resultats.some((resultat) => resultat.every((item) => item.statut === LettreStatut.BienPlace))) this._stats.partiesGagnees++;
this._stats.dernierePartie = this._datePartieEnCours;
this._sauvegardeur.sauvegarderStats(this._stats);
}
private sauvegarderPartieEnCours(): void {
let datePartieEnCours = this._datePartieEnCours ?? new Date();
this._sauvegardeur.sauvegarderPartieEnCours(this._propositions, datePartieEnCours);
}
private choisirMot(): string {
return this._dictionnaire.nettoyerMot(this._dictionnaire.getMot());
}
private decompose(mot: string): { [lettre: string]: number } {
let composition: { [lettre: string]: number } = {};
for (let position = 0; position < mot.length; position++) {
let lettre = mot[position];
if (composition[lettre]) composition[lettre]++;
else composition[lettre] = 1;
}
return composition;
}
public verifierMot(mot: string, skipAnimation: boolean = false): void {
mot = this._dictionnaire.nettoyerMot(mot);
//console.debug(mot + " => " + (this._dictionnaire.estMotValide(mot) ? "Oui" : "non"));
if (mot[0] !== this._motATrouver[0] || !this._dictionnaire.estMotValide(mot)) {
NotificationMessage.ajouterNotification("Ce mot n'est pas valide");
return;
}
if (!this._datePartieEnCours) this._datePartieEnCours = new Date();
let resultats = this.analyserMot(mot);
let isBonneReponse = resultats.every((item) => item.statut === LettreStatut.BienPlace);
this._propositions.push(mot);
this._resultats.push(resultats);
this._grille.validerMot(mot, resultats, isBonneReponse, skipAnimation);
this._input.updateClavier(resultats);
if (isBonneReponse || this._propositions.length === this._maxNbPropositions) {
this._input.bloquer();
this._victoirePanel.genererResume(isBonneReponse, this._resultats);
this._victoirePanel.afficher(isBonneReponse, this._motATrouver);
this.enregistrerPartieDansStats();
}
this.sauvegarderPartieEnCours();
}
public actualiserAffichage(mot: string): void {
this._grille.actualiserAffichage(this._dictionnaire.nettoyerMot(mot));
}
private analyserMot(mot: string): Array<LettreResultat> {
let resultats = new Array<LettreResultat>();
mot = mot.toUpperCase();
let composition = { ...this._compositionMotATrouver };
for (let position = 0; position < this._motATrouver.length; position++) {
let lettreATrouve = this._motATrouver[position];
let lettreProposee = mot[position];
if (lettreATrouve === lettreProposee) {
composition[lettreProposee]--;
}
}
for (let position = 0; position < this._motATrouver.length; position++) {
let lettreATrouve = this._motATrouver[position];
let lettreProposee = mot[position];
let resultat = new LettreResultat();
resultat.lettre = lettreProposee;
if (lettreATrouve === lettreProposee) {
resultat.statut = LettreStatut.BienPlace;
} else if (this._motATrouver.includes(lettreProposee)) {
if (composition[lettreProposee] > 0) {
resultat.statut = LettreStatut.MalPlace;
composition[lettreProposee]--;
} else {
resultat.statut = LettreStatut.NonTrouve;
}
} else {
resultat.statut = LettreStatut.NonTrouve;
}
resultats.push(resultat);
}
return resultats;
}
}

153
ts/grille.ts Normal file
View File

@ -0,0 +1,153 @@
import LettreResultat from "./lettreResultat";
import { LettreStatut } from "./lettreStatut";
export default class Grille {
private readonly _grille: HTMLElement;
private readonly _propositions: Array<string>;
private readonly _resultats: Array<Array<LettreResultat>>;
private readonly _longueurMot: number;
private readonly _maxPropositions: number;
private _indice: Array<string | undefined>;
private _motActuel: number;
public constructor(longueurMot: number, maxPropositions: number, indice: string) {
this._grille = document.getElementById("grille") as HTMLElement;
//console.log("Chargement de la grille");
this._longueurMot = longueurMot;
this._maxPropositions = maxPropositions;
this._indice = new Array<string | undefined>(longueurMot);
this._indice[0] = indice;
this._propositions = new Array<string>();
this._resultats = new Array<Array<LettreResultat>>();
this._motActuel = 0;
this.afficherGrille();
}
private afficherGrille() {
let table = document.createElement("table");
for (let nbMot = 0; nbMot < this._maxPropositions; nbMot++) {
let ligne = document.createElement("tr");
let mot = this._propositions.length <= nbMot ? "" : this._propositions[nbMot];
for (let nbLettre = 0; nbLettre < this._longueurMot; nbLettre++) {
let cellule = document.createElement("td");
let contenuCellule: string = "";
if (nbMot < this._motActuel || (nbMot === this._motActuel && mot.length !== 0)) {
if (mot.length <= nbLettre) {
contenuCellule = ".";
} else {
contenuCellule = mot[nbLettre].toUpperCase();
}
} else if (nbMot === this._motActuel) {
let lettreIndice = this._indice[nbLettre];
if (lettreIndice !== undefined) contenuCellule = lettreIndice;
else contenuCellule = ".";
}
if (this._resultats.length > nbMot && this._resultats[nbMot][nbLettre]) {
let resultat = this._resultats[nbMot][nbLettre];
let emoji: string = "🟦";
switch (resultat.statut) {
case LettreStatut.BienPlace:
emoji = "🟥";
cellule.classList.add("bien-place", "resultat");
break;
case LettreStatut.MalPlace:
emoji = "🟡";
cellule.classList.add("mal-place", "resultat");
break;
default:
emoji = "🟦";
cellule.classList.add("non-trouve", "resultat");
}
// console.log(resultat.lettre + " => " + emoji);
}
cellule.innerText = contenuCellule;
ligne.appendChild(cellule);
}
table.appendChild(ligne);
}
this._grille.innerHTML = "";
this._grille.appendChild(table);
}
public actualiserAffichage(mot: string) {
this.saisirMot(this._motActuel, mot);
this.afficherGrille();
}
public validerMot(mot: string, resultats: Array<LettreResultat>, isBonneReponse: boolean, skipAnimation: boolean = false): void {
this.saisirMot(this._motActuel, mot);
this.mettreAJourIndice(resultats);
this._resultats.push(resultats);
if (!skipAnimation) this.animerResultats(resultats);
if (isBonneReponse) {
this.bloquerGrille();
} else {
this._motActuel++;
}
if (skipAnimation) this.afficherGrille();
}
private animerResultats(resultats: Array<LettreResultat>): void {
let table = this._grille.getElementsByTagName("table").item(0);
if (table === null) {
this.afficherGrille();
return;
}
let ligne = table.getElementsByTagName("tr").item(this._motActuel);
if (ligne === null) {
this.afficherGrille();
return;
}
let td = ligne.getElementsByTagName("td");
this.animerLettre(td, resultats, 0);
}
private animerLettre(td: HTMLCollectionOf<HTMLTableCellElement>, resultats: Array<LettreResultat>, numLettre: number): void {
if (numLettre >= td.length) {
this.afficherGrille();
return;
}
let cellule = td[numLettre];
let resultat = resultats[numLettre];
cellule.innerHTML = resultat.lettre;
switch (resultat.statut) {
case LettreStatut.BienPlace:
cellule.classList.add("bien-place", "resultat");
break;
case LettreStatut.MalPlace:
cellule.classList.add("mal-place", "resultat");
break;
default:
cellule.classList.add("non-trouve", "resultat");
}
setTimeout((() => this.animerLettre(td, resultats, numLettre + 1)).bind(this), 250);
}
private mettreAJourIndice(resultats: Array<LettreResultat>): void {
for (let i = 0; i < this._indice.length; i++) {
if (!this._indice[i]) {
this._indice[i] = resultats[i].statut === LettreStatut.BienPlace ? resultats[i].lettre : undefined;
}
}
}
private saisirMot(position: number, mot: string): void {
if (this._propositions.length <= position) {
this._propositions.push("");
}
this._propositions[position] = mot;
}
private bloquerGrille(): void {}
}

154
ts/input.ts Normal file
View File

@ -0,0 +1,154 @@
import Gestionnaire from "./gestionnaire";
import LettreResultat from "./lettreResultat";
import { LettreStatut } from "./lettreStatut";
export default class Input {
private readonly _grille: HTMLElement;
private readonly _inputArea: HTMLElement;
private readonly _gestionnaire: Gestionnaire;
private _longueurMot: number;
private _motSaisi: string;
private _estBloque: boolean;
public constructor(gestionnaire: Gestionnaire, longueurMot: number) {
this._grille = document.getElementById("grille") as HTMLElement;
this._inputArea = document.getElementById("input-area") as HTMLElement;
this._longueurMot = longueurMot;
this._gestionnaire = gestionnaire;
this._motSaisi = "";
this._estBloque = false;
document.addEventListener(
"keypress",
((event: KeyboardEvent) => {
event.stopPropagation();
let touche = event.key;
if (touche === "Enter") {
this.validerMot();
} else if (touche === "Backspace") {
this.effacerLettre();
} else {
this.saisirLettre(touche);
}
}).bind(this)
);
// Le retour arrière n'est détecté que par keyup
document.addEventListener(
"keyup",
((event: KeyboardEvent) => {
event.stopPropagation();
let touche = event.key;
if (touche === "Backspace") {
this.effacerLettre();
}
}).bind(this)
);
this._inputArea.querySelectorAll(".input-lettre").forEach((lettreDiv) =>
lettreDiv.addEventListener("click", (event) => {
event.stopPropagation();
let div = event.currentTarget;
if (!div) return;
let lettre = (div as HTMLElement).dataset["lettre"];
if (lettre === undefined) {
return;
} else if (lettre === "_effacer") {
this.effacerLettre();
} else if (lettre === "_entree") {
this.validerMot();
} else {
this.saisirLettre(lettre);
}
})
);
}
private effacerLettre(): void {
if (this._estBloque) return;
if (this._motSaisi.length === 0) return;
this._motSaisi = this._motSaisi.substring(0, this._motSaisi.length - 1);
this._gestionnaire.actualiserAffichage(this._motSaisi);
}
private validerMot(): void {
if (this._estBloque) return;
let mot = this._motSaisi;
if (mot.length === this._longueurMot) {
this._gestionnaire.verifierMot(mot);
this._motSaisi = "";
}
}
private saisirLettre(lettre: string): void {
if (this._estBloque) return;
if (this._motSaisi.length >= this._longueurMot) return;
this._motSaisi += lettre;
this._gestionnaire.actualiserAffichage(this._motSaisi);
}
public bloquer(): void {
this._estBloque = true;
}
public updateClavier(resultats: Array<LettreResultat>): void {
if (this._estBloque) return;
let statutLettres: { [lettre: string]: LettreStatut } = {};
// console.log(statutLettres);
for (let resultat of resultats) {
if (!statutLettres[resultat.lettre]) statutLettres[resultat.lettre] = resultat.statut;
else {
switch (resultat.statut) {
case LettreStatut.BienPlace:
statutLettres[resultat.lettre] = LettreStatut.BienPlace;
break;
case LettreStatut.MalPlace:
if (statutLettres[resultat.lettre] !== LettreStatut.BienPlace) {
statutLettres[resultat.lettre] = LettreStatut.MalPlace;
}
break;
default:
break;
}
}
}
// console.log(statutLettres);
let touches = this._inputArea.querySelectorAll(".input-lettre");
for (let lettre in statutLettres) {
let statut = statutLettres[lettre];
for (let numTouche = 0; numTouche < touches.length; numTouche++) {
let touche = touches.item(numTouche) as HTMLElement;
if (touche === undefined || touche === null) continue;
if (touche.dataset["lettre"] === lettre) {
// console.log(lettre + " => " + statut);
switch (statut) {
case LettreStatut.BienPlace:
touche.className = "";
touche.classList.add("input-lettre");
touche.classList.add("lettre-bien-place");
break;
case LettreStatut.MalPlace:
if (touche.classList.contains("lettre-bien-place")) break;
touche.className = "";
touche.classList.add("input-lettre");
touche.classList.add("lettre-mal-place");
break;
default:
if (touche.classList.contains("lettre-bien-place")) break;
if (touche.classList.contains("lettre-mal-place")) break;
touche.className = "";
touche.classList.add("input-lettre");
touche.classList.add("lettre-non-trouve");
break;
}
break;
}
}
}
}
}

6
ts/lettreResultat.ts Normal file
View File

@ -0,0 +1,6 @@
import { LettreStatut } from "./lettreStatut";
export default class LettreResultat {
lettre: string = "";
statut: LettreStatut = LettreStatut.NonTrouve;
}

5
ts/lettreStatut.ts Normal file
View File

@ -0,0 +1,5 @@
export enum LettreStatut {
NonTrouve,
MalPlace,
BienPlace,
}

8
ts/main.ts Normal file
View File

@ -0,0 +1,8 @@
import Gestionnaire from "./gestionnaire";
export default class Main {
public constructor() {
console.log("🟥🟦🟦🟡🟡🟡🟦🟦");
let gestionnaire = new Gestionnaire();
}
}

132315
ts/mots/listeMotsProposables.ts Normal file

File diff suppressed because it is too large Load Diff

3
ts/mots/motsATrouver.ts Normal file
View File

@ -0,0 +1,3 @@
export default class MotsATrouver {
public static readonly Liste: Array<string> = ["DIFFUSION", "NEGATIVE", "ABSENCE", "LISTES"];
}

20
ts/notificationMessage.ts Normal file
View File

@ -0,0 +1,20 @@
export default class NotificationMessage {
private static _notificationArea: HTMLElement = document.getElementById("notification") as HTMLElement;
private static _currentTimeout: NodeJS.Timeout | undefined;
public static ajouterNotification(message: string): void {
if (this._currentTimeout) {
clearTimeout(this._currentTimeout);
this._currentTimeout = undefined;
}
this._notificationArea.innerHTML = message;
this._notificationArea.style.opacity = "1";
this._currentTimeout = setTimeout(
(() => {
this._notificationArea.style.opacity = "0";
this._notificationArea.innerHTML = "";
this._currentTimeout = undefined;
}).bind(this),
5000
);
}
}

4
ts/sauvegardePartie.ts Normal file
View File

@ -0,0 +1,4 @@
export default class SauvegardePartie {
propositions: Array<string> = [];
datePartie: Date = new Date();
}

5
ts/sauvegardeStats.ts Normal file
View File

@ -0,0 +1,5 @@
export default class SauvegardeStats {
dernierePartie?: Date = new Date();
partiesJouees: number = 0;
partiesGagnees: number = 0;
}

47
ts/sauvegardeur.ts Normal file
View File

@ -0,0 +1,47 @@
import SauvegardePartie from "./sauvegardePartie";
import SauvegardeStats from "./sauvegardeStats";
export default class Sauvegardeur {
public constructor() {}
public sauvegarderStats(stats: SauvegardeStats): void {
localStorage.setItem("stats", JSON.stringify(stats));
}
public chargerSauvegardeStats(): SauvegardeStats | undefined {
let dataStats = localStorage.getItem("stats");
if (!dataStats) return;
let stats = JSON.parse(dataStats) as SauvegardeStats;
return stats;
}
public sauvegarderPartieEnCours(propositions: Array<string>, datePartie: Date): void {
let partieEnCours: SauvegardePartie = {
propositions: propositions,
datePartie,
};
localStorage.setItem("partieEnCours", JSON.stringify(partieEnCours));
}
public chargerSauvegardePartieEnCours(): { propositions: Array<string>; datePartie: Date } | undefined {
let dataPartieEnCours = localStorage.getItem("partieEnCours");
if (!dataPartieEnCours) return;
let partieEnCours = JSON.parse(dataPartieEnCours) as SauvegardePartie;
let aujourdhui = new Date();
let datePartieEnCours = new Date(partieEnCours.datePartie);
if (
aujourdhui.getDate() !== datePartieEnCours.getDate() ||
aujourdhui.getMonth() !== datePartieEnCours.getMonth() ||
aujourdhui.getFullYear() !== datePartieEnCours.getFullYear()
) {
localStorage.removeItem("partieEnCours");
return;
}
return {
datePartie: datePartieEnCours,
propositions: partieEnCours.propositions,
};
}
}

18
ts/server.ts Normal file
View File

@ -0,0 +1,18 @@
import express from "express";
import http from "http";
const app = express();
const port = 4000;
(async () => {
app.use("/", express.static("public/"));
app.use("/js", express.static("js/"));
app.use("/ts", express.static("ts/"));
app.use("/node_modules/requirejs/require.js", express.static("node_modules/requirejs/require.js"));
app.use(express.json());
const server = http.createServer(app);
server.listen(port, () => {
console.log(`Jeu démarré : http://localhost:${port}`);
});
})();

64
tsconfig.json Normal file
View File

@ -0,0 +1,64 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "umd" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true /* Generates corresponding '.map' file. */,
//"outFile": "./js/index.js" /* Concatenate and emit output to single file. */,
"outDir": "./js/" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"moduleResolution": "node"
},
"include": ["ts/*.ts", "type/*"]
}

52
utils/genererMotTrouve.js Normal file
View File

@ -0,0 +1,52 @@
"use strict";
/**
* Petit script qui permet de remplir rapidement (mais manuellement) la liste des fichiers à trouver.
*/
var fs = require("fs");
var readlineSync = require("readline-sync");
function start() {
let motsGardes = [];
fs.readFile("public/mots.txt", "UTF8", function (erreur, contenu) {
//console.log(erreur);
var dictionnaire = contenu.split("\n");
while (true) {
var motTrouve = false;
var mot = "";
do {
var position = Math.floor(Math.random() * dictionnaire.length);
mot = dictionnaire[position];
let motAnalyse = mot.normalize("NFD").replace(/\p{Diacritic}/gu, "");
motTrouve =
!(motAnalyse[0] === motAnalyse[0].toUpperCase()) &&
motAnalyse.length >= 6 &&
motAnalyse.length <= 9 &&
!motAnalyse.includes("!") &&
!motAnalyse.includes(" ") &&
!motAnalyse.includes("-") &&
!mot.toUpperCase().startsWith("K") &&
!mot.toUpperCase().startsWith("Q") &&
!mot.toUpperCase().startsWith("W") &&
!mot.toUpperCase().startsWith("X") &&
!mot.toUpperCase().startsWith("Y") &&
!mot.toUpperCase().startsWith("Z");
} while (!motTrouve);
console.log(mot);
let reponse = readlineSync.question("On garde ? [O]ui ou [N]on (ou [STOP])\n");
if (reponse.toLowerCase() === "stop") break;
let isGarde = reponse.toLowerCase() === "o";
if (isGarde) motsGardes.push(mot);
}
fs.appendFile("public/motsATrouve.txt", motsGardes.join("\n") + "\n", (err) => {
if (err) {
console.error(err);
return;
}
//file written successfully
});
});
}
start();

41
utils/nettoyage.js Normal file
View File

@ -0,0 +1,41 @@
"use strict";
/**
* Petit script qui nettoie le fichier dictionnaire pour le mettre dans le format attendu par le système
*/
var fs = require("fs");
fs.readFile("public/mots.txt", "UTF8", function (erreur, contenu) {
//console.log(erreur);
var dictionnaire = contenu.split("\n");
contenu = "private readonly _dictionnaire: Array<string> = [\n";
contenu += dictionnaire
.map((mot) => mot.normalize("NFD").replace(/\p{Diacritic}/gu, ""))
.filter(
(mot) =>
!(mot[0] === mot[0].toUpperCase()) &&
mot.length >= 6 &&
mot.length <= 9 &&
!mot.includes("!") &&
!mot.includes(" ") &&
!mot.includes("-") &&
!mot.toUpperCase().startsWith("K") &&
!mot.toUpperCase().startsWith("Q") &&
!mot.toUpperCase().startsWith("W") &&
!mot.toUpperCase().startsWith("X") &&
!mot.toUpperCase().startsWith("Y") &&
!mot.toUpperCase().startsWith("Z")
)
.map(function (mot) {
return '"' + mot.toUpperCase() + '",';
})
.join("\n");
contenu += "\n]";
fs.writeFile("public/motsNettoyes.txt", contenu, function (err) {
if (err) {
console.error(err);
return;
}
//file written successfully
});
});

View File

@ -0,0 +1,77 @@
"use strict";
/**
* Petit script qui nettoie le fichier des mots à trouver pour le mettre dans le format attendu par le système
*/
var fs = require("fs");
function shuffle(array) {
let currentIndex = array.length,
randomIndex;
// While there remain elements to shuffle...
while (currentIndex != 0) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
return array;
}
let aujourdhui = new Date().getTime();
let origine = new Date(2022, 0, 8).getTime();
let numeroGrille = Math.floor((aujourdhui - origine) / (24 * 3600 * 1000));
const maxFige = numeroGrille; // inclus
fs.readFile("public/motsATrouve.txt", "UTF8", function (erreur, contenu) {
//console.log(erreur);
var dictionnaire = contenu.split("\n");
let motsFiges = dictionnaire.slice(0, maxFige + 1);
let motsMelanges = shuffle(dictionnaire.slice(maxFige + 1));
contenu = "private readonly _motATrouve: Array<string> = [\n";
contenu +=
motsFiges
.map(
(mot) =>
'"' +
mot
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "")
.toUpperCase() +
'",'
)
.join("\n") + "\n";
contenu += motsMelanges
.map((mot) => mot.normalize("NFD").replace(/\p{Diacritic}/gu, ""))
.filter(
(mot) =>
mot &&
mot.length >= 6 &&
mot.length <= 9 &&
!mot.includes("!") &&
!mot.includes(" ") &&
!mot.includes("-") &&
!mot.toUpperCase().startsWith("K") &&
!mot.toUpperCase().startsWith("Q") &&
!mot.toUpperCase().startsWith("W") &&
!mot.toUpperCase().startsWith("X") &&
!mot.toUpperCase().startsWith("Y") &&
!mot.toUpperCase().startsWith("Z")
)
.map(function (mot) {
return '"' + mot.toUpperCase() + '",';
})
.join("\n");
contenu += "\n]";
fs.writeFile("public/motsATrouveNettoyes.txt", contenu, function (err) {
if (err) {
console.error(err);
return;
}
//file written successfully
});
});

14
utils/seed.js Normal file
View File

@ -0,0 +1,14 @@
"use strict";
/**
* Script qui n'est plus vraiment utile, et qui permet de vérifier qu'un générateur de seed navigue bien dans la liste des mots
*/
var dateJour = new Date();
var aujourdhui = dateJour.getFullYear() * 10000 + (dateJour.getMonth() + 1) * 10 + dateJour.getDate();
var seed = 18;
aujourdhui += seed;
for (var i = 0; i < 10; i++) {
var positionRandom = (aujourdhui * aujourdhui) % 112768;
//console.log(positionRandom);
aujourdhui++;
}

38
utils/stats.js Normal file
View File

@ -0,0 +1,38 @@
"use strict";
/**
* Petit script pour avoir quelques stats sur la liste des mots à trouver
*/
var fs = require("fs");
fs.readFile("public/motsATrouve.txt", "UTF8", function (erreur, contenu) {
//console.log(erreur);
var dictionnaire = contenu.split("\n");
let lettres = {};
for (let mot of dictionnaire) {
if (!mot) continue;
let initiale = mot[0].toUpperCase();
let motClean = mot.normalize("NFD").replace(/\p{Diacritic}/gu, "");
let longueur = motClean.length;
if (lettres[initiale] === undefined) lettres[initiale] = { 6: 0, 7: 0, 8: 0, 9: 0 };
lettres[initiale][longueur.toString()]++;
}
console.log(" | 6 | 7 | 8 | 9 |");
for (let lettre in lettres) {
let stats = lettres[lettre];
console.log(
lettre +
" | " +
stats["6"].toString().padStart(3) +
" | " +
stats["7"].toString().padStart(3) +
" | " +
stats["8"].toString().padStart(3) +
" | " +
stats["9"].toString().padStart(3) +
" |"
);
}
});