From 9122e245a50b30ca845c5839461a3d2511223a9a Mon Sep 17 00:00:00 2001 From: Didictateur Date: Sun, 30 Mar 2025 23:57:10 +0200 Subject: [PATCH] Premiers yakus + tests --- Makefile | 2 + index.html | 11 +- src/group.ts | 8 + src/hand.ts | 4 + src/tests/assert.ts | 9 + src/tests/test_yakus.ts | 42 +++ src/tile.ts | 14 + src/yakus/yaku.ts | 549 ++++++++++++++++++++++++++++++++++++++++ webpack.config.js | 8 + 9 files changed, 644 insertions(+), 3 deletions(-) create mode 100644 src/tests/assert.ts create mode 100644 src/tests/test_yakus.ts create mode 100644 src/yakus/yaku.ts diff --git a/Makefile b/Makefile index b88001b..9754ad5 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +TESTS = $(wildcard build/test*) + all: npx webpack --mode development diff --git a/index.html b/index.html index e7e4063..45229b2 100644 --- a/index.html +++ b/index.html @@ -139,7 +139,7 @@ document.body.appendChild(currentScript); } - function loadScript(scriptName) { + function loadScript(scriptName, txt = true) { if (window.cleanup) { window.cleanup(); } @@ -155,10 +155,15 @@ document.body.appendChild(currentScript); - const number = scriptName.substring(2, scriptName.length - 3); - loadText("txt" + number + ".js"); + if (txt) { + const number = scriptName.substring(2, scriptName.length - 3); + loadText("txt" + number + ".js"); + } } + //tests + loadScript('test_yakus.js', false); + //script initial loadScript('dp5.js'); diff --git a/src/group.ts b/src/group.ts index 470fe7d..812e85a 100644 --- a/src/group.ts +++ b/src/group.ts @@ -27,6 +27,14 @@ export class Group { return this.tiles; } + public compare(g: Group): number { + let c = this.tiles[0].compare(g.tiles[0]); + if (c !== 0) { + return c; + } + return this.tiles[1].compare(g.tiles[1]); + } + public drawGroup( ctx: CanvasRenderingContext2D, x: number, diff --git a/src/hand.ts b/src/hand.ts index e47c40a..d68eebc 100644 --- a/src/hand.ts +++ b/src/hand.ts @@ -23,6 +23,10 @@ export class Hand { } } + public getTiles(): Array { + return this.tiles; + } + public length(): number { return this.tiles.length; } diff --git a/src/tests/assert.ts b/src/tests/assert.ts new file mode 100644 index 0000000..57ca753 --- /dev/null +++ b/src/tests/assert.ts @@ -0,0 +1,9 @@ +export function assert(b: boolean, msg: string): number { + if (b) { + console.log("%c[SUCCES] " + msg, "color: green"); + return 1; + } else { + console.log("%c[ECHEC] " + msg, "color: red"); + return 0; + } +} diff --git a/src/tests/test_yakus.ts b/src/tests/test_yakus.ts new file mode 100644 index 0000000..f2be87a --- /dev/null +++ b/src/tests/test_yakus.ts @@ -0,0 +1,42 @@ +import { assert } from "./assert" +import { yakus } from "../yakus/yaku" +import { Hand } from "../hand" + +let count = 0; +let total = 0; + +let h1 = new Hand("m1m2m3 m4m5m6 m7m8m9 p1p2p3 p5p5"); +let h2 = new Hand("m1m1m1 m4m4m4 m7m7m7 p1p1p1 p5p5"); +let h3 = new Hand("m1m1m1p9p9p9s1s1s1w2w2w2d1d1"); +let h4 = new Hand("m2m3m4 p3p3p3 p4p5p6 s4s4 s6s7s8"); +let h5 = new Hand("m1m2m3m1m2m3p7p8p9p7p8p9w3w3"); +let h6 = new Hand("m1m2m3 m1m2m3 p6p7p8 p7p8p9 w3w3"); +let h7 = new Hand("m1m2m3 p1p2p3 s1s2s3 m9m9m9 p9p9"); + +// lipeikou +count += assert(yakus.lipekou(h5, [], 0) === 1, "m123 m123 p789 p789 w33 is Lipeikou"); +count += assert(yakus.lipekou(h3, [], 0) === 0, "m111 p999 s111 w222 d11 is not Lipeikou"); +total += 2; + +// ryanpeikou +count += assert(yakus.ryanpeikou(h5, [], 0) === 3, "m123 m123 p789 p789 w33 is Ryanpeikou"); +count += assert(yakus.ryanpeikou(h6, [], 0) === 0, "m1123 m123 p678 p789 w33 is not Ryanpeikou"); +total += 2; + +// pinfu +count += assert(yakus.pinfu(h1, [], 0) === 1, "m123 m456 m789 m123 p55 is Pinfu"); +count += assert(yakus.pinfu(h2, [], 0) === 0, "m111 m444 m777 p111 p55 is not Pinfu"); +total += 2; + +// sanshoku doujun +count += assert(yakus.sanshokuDoujun(h7, [], 0) === 2, "m123 p123 s123 m999 p99 is Shanshokou Doujun"); +count += assert(yakus.sanshokuDoujun(h2, [], 0) === 0, "m111 m444 m777 p111 p55 is not Shanshokou Doujun"); +total += 2; + +// chanta +count += assert(yakus.chanta(h3, [], 0) === 2, "m111 p999 s111 w222 d11 is Chanta"); +count += assert(yakus.chanta(h1, [], 0) === 0, "m123 m456 m789 m123 p55 is not Chanta"); +total += 2; + +// total +console.log("Succès: " + count.toString() + "/" + total.toString()); diff --git a/src/tile.ts b/src/tile.ts index b708901..3744f83 100644 --- a/src/tile.ts +++ b/src/tile.ts @@ -38,6 +38,20 @@ export class Tile { return this.red; } + public compare(t: Tile): number { + if (this.family < t.family) { + return -1; + } else if (this.family > t.family) { + return 1; + } + if (this.value < t.value) { + return -1; + } else if (this.value > t.value) { + return 1; + } + return 0; + } + public setTilt(): void { this.tilt = (1 - 2 * Math.random()) * 0.04; } diff --git a/src/yakus/yaku.ts b/src/yakus/yaku.ts new file mode 100644 index 0000000..ed06927 --- /dev/null +++ b/src/yakus/yaku.ts @@ -0,0 +1,549 @@ +import { Hand } from "../hand"; +import { Group } from "../group"; + +function ord(g: Group): boolean { + return g.getTiles().every( + t => t.getFamily() < 4 && + t.getValue() > 1 && + t.getValue() < 9 + ) +} + +function term(g: Group): boolean { + return g.getTiles().every( + t => + t.getFamily() < 4 && + (t.getValue() === 0 || + t.getValue() === 9) + ); +} + +function honn(g: Group): boolean { + return g.getTiles().every( + t => + t.getFamily() >= 4 + ); +} + +function chii(g: Group): boolean { + let t = g.getTiles(); + return t[0].getValue() !== t[1].getValue(); +} + +function pon(g: Group): boolean { + let t = g.getTiles(); + return t[0].getValue() === t[1].getValue(); +} + + + + + +export const yakus = { + +lipekou: function ( + /** + * double suite pure + * 0/1 + */ + hand: Hand, + groups: Array, + wind: number +): number { + if (groups.length > 0 && !yakus.ryanpeikou(hand, groups, wind)) { // ouvert + return 0; + } + let gr = hand.toGroup() as NonNullable>; + gr.sort((g1, g2) => g1.compare(g2)); + for (let i = 0; i < 3; i++) { + let g1 = gr[i].getTiles(); + let g2 = gr[i+1].getTiles(); + if ( + g1[0].isEqual(g2[0].getFamily(), g2[0].getValue()) && + chii(gr[i]) && chii(gr[i+1]) + ) { + return 1; + } + } + return 0; +}, + +ryanpeikou: function ( + /** + * deux doubles suites pures + * 0/3 + */ + hand: Hand, + groups: Array, + wind: number +): number { + if (groups.length > 0) { + return 0; + } + let gr = hand.toGroup() as NonNullable>; + gr.filter(g => chii(g)); + if (gr.length < 4) { // pas assez de suite + return 0; + } + gr.sort((g1, g2) => g1.compare(g2)); + let t1 = gr[0].getTiles()[0]; + let t2 = gr[1].getTiles()[0]; + let t3 = gr[2].getTiles()[0]; + let t4 = gr[3].getTiles()[0]; + if ( + t1.compare(t2) === 0 && + t3.compare(t4) === 0 + ) { + return 3; + } + return 0; +}, + +pinfu: function( + /** + * tout suite + * 0/1 + */ + hand: Hand, + groups: Array, + wind: number +): number { //TODO: double attente + if (groups.length > 0) { + return 0; + } + let h = hand.toGroup(); + if ( + h !== undefined && + h.every( + g => { + let tiles = g.getTiles(); + return (chii(g) || tiles.length === 2) + } + ) + ) { + return 1; + } + return 0; +}, + +sanshokuDoujun: function( // TODO + /** + * triple suite + * 1/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +ittsuu: function( + /** + * grande suite pure + * 1/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + let gr = groups.concat(hand.toGroup() as NonNullable>); + gr.sort((g1, g2) => g1.compare(g2)); + gr.filter(g => chii(g)); + if (gr.length < 3) { // trop peu de suite + return 0; + } else if (gr.length === 3) { // pile le bon nombre + let g1 = gr[0].getTiles(); + let g2 = gr[1].getTiles(); + let g3 = gr[2].getTiles(); + if ( + g1[0].getFamily() === g2[0].getFamily() && + g2[0].getFamily() === g3[0].getFamily() && + g1[0].getValue() === 1 && + g2[0].getValue() === 4 && + g3[0].getValue() === 7 + ) { + return groups.length > 0 ? 1 : 2; + } + } else { // il y a un intrus + return 0 //TODO + } + return 0; +}, + +tanyao: function( + /** + * tout ordinaire + * 1/1 + */ + hand: Hand, + groups: Array, + wind: number +): number { + let h = hand.toGroup(); + if (h === undefined) { + return 0; + } + if ( + groups.every(g => ord(g)) && + h.every(g => ord(g)) + ) { + return 1; + } + return 0; +}, + +yakuhai: function( + /** + * brelan de valeur + * 1/1 + */ + // TODO multiplicité ! + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +shousangen: function( //TODO + /** + * trois petits dragons + * 2/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +daisangen: function( //TODO + /** + * trois grands dragons + * 13/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +shousuushii: function( //TODO + /** + * quatre petits vents + * 13/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +daisuushi: function( //TODO + /** + * quatre grands vents + * 13/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +chanta: function( //TODO + /** + * terminales et honneurs partout + * 1/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +junchan: function( //TODO + /** + * terminales partout + * 2/3 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +honroutou: function( //TODO + /** + * tout terminale et honneur + * 2/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +chinroutou: function( + /** + * tout terminale + * 13/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + let h = hand.toGroup(); + if (h === undefined) { + return 0; + } + if ( + groups.every(g => term(g)) && + h.every(g => term(g)) + ) { + return 13; + } + return 0; +}, + +tsuuiisou: function( + /** + * tout honneur + * 13/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + let h = hand.toGroup(); + if (h === undefined) { + return 0; + } + if ( + groups.every(g => honn(g)) && + h.every(g => honn(g)) + ) { + return 13; + } + return 0; +}, + +kokushiMusou: function( + /** + * treize orphelins + * 0/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + if (groups.length > 0) { + return 0; + } + if (yakus.honroutou(hand, groups, wind) === 0) { + return 0; + } + let h = hand.getTiles(); + let count = 0; + for (let i = 0; i < h.length - 1; i++) { + if (h[i].isEqual(h[i+1].getFamily(), h[i+1].getValue())) { + count++; + } + if (count > 1) { + break; + } + } + if (count === 1) { + return 13; + } + return 0; +}, + +chiitoitsu: function( + /** + * sept paires + * 0/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; + if (groups.length > 0) { + return 0; + } + //TODO +}, + +toitoi: function( + /** + * tout brelans + * 2/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + let h = hand.toGroup(); + if ( + groups.every(g => pon(g)) && + h?.every(g => pon(g)) + ) { + return 2; + } + return 0; +}, + +sanankou: function( + /** + * trois brelan cachés + * 2/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + let h = hand.toGroup(); + let count = 0; + h?.forEach( + g => { + if (pon(g) && g.getTiles().length === 3) { + count++; + } + } + ); + if (count === 3) { + return 2; + } + return 0; +}, + +suuankou: function( + /** + * quatre brelans cachés + * 0/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + let h = hand.toGroup(); + let count = 0; + h?.forEach( + g => { + if (pon(g) && g.getTiles().length === 3) { + count++; + } + } + ); + if (count === 4) { + return 13; + } + return 0; +}, + +sanshokuDoukou: function( //TODO + /** + * triple brelan + * 2/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +sankantsu: function( //TODO + /** + * trois carrés + * 2/2 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +suukantsu: function( //TODO + /** + * quatre carrés + * 13/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +honitsu: function( //TODO + /** + * semie pure + * 2/3 + */ + hand: Hand, + groups: Array, + wind: number +): number { + return 0; +}, + +chinitsu: function( + /** + * main pure + * 5/6 + */ + hand: Hand, + groups: Array, + wind: number +): number { + let h = hand.getTiles(); + let t0 = h[0]; + if ( + h.every(t => t.getFamily() === t0.getFamily()) && + groups.every(g => g.getTiles().every(t => t.getFamily() === t0.getFamily())) + ) { + return groups.length > 0 ? 5 : 6; + } + return 0; +}, + +ryuuisou: function( //TODO + /** + * main verte + * 13/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + if (yakus.chinitsu(hand, groups, wind) === 0) { + return 0; + } + return 0; +}, + +chuurenPoutou: function( //TODO + /** + * neuf portes + * 0/13 + */ + hand: Hand, + groups: Array, + wind: number +): number { + if (groups.length > 0 || yakus.chinitsu(hand, groups, wind) === 0) { + return 0; + } + return 0; +} + +} diff --git a/webpack.config.js b/webpack.config.js index 6d24fcd..d8d6ee1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,8 +5,10 @@ const fs = require('fs'); function getEntryPoints() { const displayDir = path.resolve(__dirname, 'src', 'display'); const textDir = path.resolve(__dirname, 'src', 'text'); + const testDir = path.resolve(__dirname, 'src', 'tests'); const files = fs.readdirSync(displayDir); // Lire les fichiers du répertoire const texts = fs.readdirSync(textDir); + const tests = fs.readdirSync(testDir); const entryPoints = {}; files.forEach((file) => { @@ -21,6 +23,12 @@ function getEntryPoints() { entryPoints[name] = path.join(textDir, file); } }); + tests.forEach((file) => { + if (file.startsWith('test') && file.endsWith('.ts')) { + const name = path.basename(file, '.ts'); + entryPoints[name] = path.join(testDir, file); + } + }); return entryPoints; }