├── .dockerignore
├── .editorconfig
├── .env
├── .env.production
├── .eslintrc.cjs
├── .github
└── workflows
│ ├── continous-integration.yml
│ └── publish.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── deploy
├── ingress.yml
├── namespace.yml
└── web.yml
├── index.html
├── nginx
└── default.conf
├── package.json
├── package.json.note
├── public
├── og-image.png
└── site.webmanifest
├── src
├── App.vue
├── assets
│ ├── style.css
│ └── tile-icons
│ │ ├── sprite.2x.png
│ │ ├── sprite.css
│ │ └── sprite.png
├── components
│ ├── AdvancedCalculator.vue
│ ├── AdvancedCalculatorScoreModal.vue
│ ├── ConfigurationModal.vue
│ ├── DoraCounter.vue
│ ├── MahjongCombination.vue
│ ├── MahjongTile.vue
│ ├── ModalBase.vue
│ ├── SimpleCalculator.vue
│ ├── SimpleCalculatorScoreModal.vue
│ └── TileSelectionModal.vue
├── core
│ ├── combination-classes.js
│ ├── ema
│ │ ├── ema-fu-calculator.js
│ │ ├── ema-han-calculator.js
│ │ └── ema-point-calculator.js
│ ├── fu-calculation
│ │ ├── fu-calculation-context.js
│ │ ├── fu-calculation-result.js
│ │ ├── fu-calculator.js
│ │ ├── fu-info.js
│ │ ├── fu-rule.js
│ │ ├── index.js
│ │ └── rules
│ │ │ ├── chiitoitsu-fu-rule.js
│ │ │ ├── closed-ron-fu-rule.js
│ │ │ ├── combinations-fu-rule.js
│ │ │ ├── open-pinfu-fu-rule.js
│ │ │ ├── pair-fu-rule.js
│ │ │ ├── tsumo-fu-rule.js
│ │ │ ├── wait-fu-rule.js
│ │ │ └── winning-fu-rule.js
│ ├── han-calculation
│ │ ├── han-calculation-result.js
│ │ ├── han-calculator.js
│ │ ├── han-info.js
│ │ ├── index.js
│ │ ├── yaku.js
│ │ └── yakus
│ │ │ ├── chankan-yaku.js
│ │ │ ├── chanta-yaku.js
│ │ │ ├── chiihou-yaku.js
│ │ │ ├── chiitoitsu-yaku.js
│ │ │ ├── chinitsu-yaku.js
│ │ │ ├── chinroutou-yaku.js
│ │ │ ├── chuuren-poutou-yaku.js
│ │ │ ├── daisangen-yaku.js
│ │ │ ├── daisuushii-yaku.js
│ │ │ ├── double-riichi-yaku.js
│ │ │ ├── haitei-raoyue-yaku.js
│ │ │ ├── honitsu-yaku.js
│ │ │ ├── honroutou-yaku.js
│ │ │ ├── houtei-raoyui-yaku.js
│ │ │ ├── iipeikou-yaku.js
│ │ │ ├── ippatsu-yaku.js
│ │ │ ├── ittsuu-yaku.js
│ │ │ ├── junchan-yaku.js
│ │ │ ├── kokushi-musou-yaku.js
│ │ │ ├── menzen-tsumo-yaku.js
│ │ │ ├── open-riichi-yaku.js
│ │ │ ├── pinfu-yaku.js
│ │ │ ├── renhou-yaku.js
│ │ │ ├── riichi-yaku.js
│ │ │ ├── rinshan-kaihou-yaku.js
│ │ │ ├── ryanpeikou-yaku.js
│ │ │ ├── ryuuiisou-yaku.js
│ │ │ ├── sanankou-yaku.js
│ │ │ ├── sankantsu-yaku.js
│ │ │ ├── sanshoku-doujun-yaku.js
│ │ │ ├── sanshoku-doukou-yaku.js
│ │ │ ├── shousangen-yaku.js
│ │ │ ├── shousuushii-yaku.js
│ │ │ ├── suuankou-yaku.js
│ │ │ ├── suukantsu-yaku.js
│ │ │ ├── tanyao-yaku.js
│ │ │ ├── tenhou-yaku.js
│ │ │ ├── toitoi-yaku.js
│ │ │ ├── tsuuiisou-yaku.js
│ │ │ └── yakuhai-yaku.js
│ ├── hand.js
│ ├── point-calculation
│ │ └── point-calculator.js
│ ├── tile-classes.js
│ ├── waits
│ │ ├── is-kanchan-wait.js
│ │ ├── is-penchan-wait.js
│ │ └── is-tanki-wait.js
│ └── wrc
│ │ ├── index.js
│ │ ├── wrc-fu-calculator.js
│ │ ├── wrc-han-calculator.js
│ │ └── wrc-point-calculator.js
├── event-bus.js
├── filters
│ ├── format-number.js
│ └── title-case.js
├── i18n.js
├── locales
│ ├── en.json
│ └── jp-romanized.json
├── main.js
├── ruleset.js
└── rulesets
│ ├── custom-ruleset.js
│ ├── ema-ruleset.js
│ ├── helpers
│ └── four-player-game-tiles.js
│ └── wrc-ruleset.js
├── tests
├── .eslintrc.cjs
├── fixtures
│ ├── dealer-score-fixture.json
│ └── non-dealer-score-fixture.json
├── integration
│ ├── ema
│ │ ├── ema-fu-calculator.spec.js
│ │ ├── ema-han-calculator.spec.js
│ │ └── ema-point-calculator.spec.js
│ └── wrc
│ │ ├── wrc-fu-calculator.spec.js
│ │ ├── wrc-han-calculator.spec.js
│ │ └── wrc-point-calculator.spec.js
└── unit
│ ├── combination-classes.spec.js
│ ├── fu-calculation
│ ├── fu-calculator.spec.js
│ └── rules
│ │ ├── chiitoitsu-fu-rule.spec.js
│ │ ├── closed-ron-fu-rule.spec.js
│ │ ├── combinations-fu-rule.spec.js
│ │ ├── open-pinfu-fu-rule.spec.js
│ │ ├── pair-fu-rule.spec.js
│ │ ├── tsumo-fu-rule.spec.js
│ │ ├── wait-fu-rule.spec.js
│ │ └── winning-fu-rule.spec.js
│ ├── han-calculation
│ ├── han-calculator.spec.js
│ └── yakus
│ │ ├── chankan-yaku.spec.js
│ │ ├── chanta-yaku.spec.js
│ │ ├── chiihou-yaku.spec.js
│ │ ├── chiitoitsu-yaku.spec.js
│ │ ├── chinitsu-yaku.spec.js
│ │ ├── chinroutou-yaku.spec.js
│ │ ├── chuuren-poutou-yaku.spec.js
│ │ ├── daisangen-yaku.spec.js
│ │ ├── daisuushii-yaku.spec.js
│ │ ├── double-riichi-yaku.spec.js
│ │ ├── haitei-raoyue-yaku.spec.js
│ │ ├── honitsu-yaku.spec.js
│ │ ├── honroutou-yaku.spec.js
│ │ ├── houtei-raoyui-yaku.spec.js
│ │ ├── iipeikou-yaku.spec.js
│ │ ├── ippatsu-yaku.spec.js
│ │ ├── ittsuu-yaku.spec.js
│ │ ├── junchan-yaku.spec.js
│ │ ├── kokushi-musou-yaku.spec.js
│ │ ├── menzen-tsumo-yaku.spec.js
│ │ ├── open-riichi-yaku.spec.js
│ │ ├── pinfu-yaku.spec.js
│ │ ├── renhou-yaku.spec.js
│ │ ├── riichi-yaku.spec.js
│ │ ├── rinshan-kaihou-yaku.spec.js
│ │ ├── ryanpeikou-yaku.spec.js
│ │ ├── ryuuiisou-yaku.spec.js
│ │ ├── sanankou-yaku.spec.js
│ │ ├── sankantsu-yaku.spec.js
│ │ ├── sanshoku-doujun-yaku.spec.js
│ │ ├── sanshoku-doukou-yaku.spec.js
│ │ ├── shousangen-yaku.spec.js
│ │ ├── shousuushii-yaku.spec.js
│ │ ├── suuankou-yaku.spec.js
│ │ ├── suukantsu-yaku.spec.js
│ │ ├── tanyao-yaku.spec.js
│ │ ├── tenhou-yaku.spec.js
│ │ ├── toitoi-yaku.spec.js
│ │ ├── tsuuiisou-yaku.spec.js
│ │ └── yakuhai-yaku.spec.js
│ ├── hand.spec.js
│ ├── point-calculation
│ └── point-calculator.spec.js
│ ├── tiles.spec.js
│ └── waits
│ ├── is-kanchan-wait.spec.js
│ ├── is-penchan-wait.spec.js
│ └── is-tanki-wait.spec.js
├── vite.config.js
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = false
9 |
10 | [*.js]
11 | insert_final_newline = true
12 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | VITE_BASE_URL=/
2 | VITE_BASE_URL_FULL=http://localhost:3000/
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | VITE_BASE_URL=/riichi/pointer/
2 | VITE_BASE_URL_FULL=https://tools.phil.moe/riichi/pointer/
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | 'vue/setup-compiler-macros': true
5 | },
6 | extends: [
7 | 'plugin:vue/vue3-essential',
8 | 'standard'
9 | ],
10 | ignorePatterns: ['tests/*']
11 | }
12 |
--------------------------------------------------------------------------------
/.github/workflows/continous-integration.yml:
--------------------------------------------------------------------------------
1 | name: Continous Integration
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - uses: actions/setup-node@v4
18 | with:
19 | node-version: 20
20 |
21 | - name: Install Yarn
22 | run: |
23 | npm i -g yarn
24 |
25 | - name: Install dependencies
26 | run: |
27 | yarn --frozen-lockfile --production=false
28 |
29 | - name: Run linter
30 | run: |
31 | yarn lint
32 |
33 | - name: Run unit tests
34 | run: |
35 | yarn test:unit
36 |
37 | - name: Test application build
38 | run: |
39 | yarn build
40 |
41 | - name: Test Docker build
42 | run: |
43 | docker build -t ci:local .
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 |
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: 20
16 |
17 | - name: Install Yarn
18 | run: |
19 | npm i -g yarn
20 |
21 | - name: Install dependencies
22 | run: |
23 | yarn --frozen-lockfile --production=false
24 |
25 | - name: Build application
26 | run: |
27 | yarn build
28 |
29 | - name: Build docker image
30 | run: |
31 | docker build -t emeraldcoder/riichi-pointer-web:${{ github.event.release.tag_name }} .
32 |
33 | - name: Publish docker image
34 | run: |
35 | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }}
36 | docker push emeraldcoder/riichi-pointer-web:${{ github.event.release.tag_name }}
37 |
38 | - name: Update K8S cluster
39 | run: |
40 | cat <<< "${{ secrets.DO_K8S_CONFIG }}" > $GITHUB_WORKSPACE/.kubeconfig
41 | IMAGE_TAG=${{ github.event.release.tag_name }} envsubst < $GITHUB_WORKSPACE/deploy/web.yml > $GITHUB_WORKSPACE/deploy/web.processed.yml
42 | kubectl --kubeconfig=$GITHUB_WORKSPACE/.kubeconfig apply -f $GITHUB_WORKSPACE/deploy/namespace.yml
43 | kubectl --kubeconfig=$GITHUB_WORKSPACE/.kubeconfig apply -f $GITHUB_WORKSPACE/deploy/web.processed.yml
44 | kubectl --kubeconfig=$GITHUB_WORKSPACE/.kubeconfig apply -f $GITHUB_WORKSPACE/deploy/ingress.yml
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:alpine
2 | ADD ./dist /usr/share/nginx/html
3 | ADD ./nginx /etc/nginx/conf.d
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 EmeraldCoder
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/deploy/ingress.yml:
--------------------------------------------------------------------------------
1 | apiVersion: networking.k8s.io/v1
2 | kind: Ingress
3 | metadata:
4 | name: riichi-pointer
5 | namespace: riichi
6 | annotations:
7 | kubernetes.io/ingress.class: "nginx"
8 | nginx.ingress.kubernetes.io/rewrite-target: /$1
9 | nginx.ingress.kubernetes.io/service-upstream: "true"
10 | spec:
11 | rules:
12 | - host: tools.phil.moe
13 | http:
14 | paths:
15 | - path: /riichi/pointer/?(.*)
16 | pathType: Prefix
17 | backend:
18 | service:
19 | name: riichi-pointer-web
20 | port:
21 | number: 80
--------------------------------------------------------------------------------
/deploy/namespace.yml:
--------------------------------------------------------------------------------
1 | kind: Namespace
2 | apiVersion: v1
3 | metadata:
4 | name: riichi
5 | labels:
6 | name: riichi
--------------------------------------------------------------------------------
/deploy/web.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: riichi-pointer-web
5 | namespace: riichi
6 | labels:
7 | app: riichi-pointer-web
8 | spec:
9 | replicas: 1
10 | selector:
11 | matchLabels:
12 | app: riichi-pointer-web
13 | template:
14 | metadata:
15 | labels:
16 | app: riichi-pointer-web
17 | spec:
18 | containers:
19 | - name: riichi-pointer-web
20 | image: emeraldcoder/riichi-pointer-web:${IMAGE_TAG}
21 | ports:
22 | - containerPort: 80
23 | imagePullSecrets:
24 | - name: regcred
25 |
26 | ---
27 |
28 | apiVersion: v1
29 | kind: Service
30 | metadata:
31 | name: riichi-pointer-web
32 | namespace: riichi
33 | spec:
34 | selector:
35 | app: riichi-pointer-web
36 | ports:
37 | - name: tcp
38 | port: 80
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Riichi Mahjong Pointer (beta version)
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/nginx/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name localhost;
4 |
5 | location / {
6 | root /usr/share/nginx/html;
7 | index index.html;
8 |
9 | # Default caching strategy based on the etag feature (for stuff like index.html and site.webmanifest that don't have a content hash in the name)
10 | etag on;
11 | add_header Cache-Control "no-cache";
12 |
13 | # Caching strategy for static assets (Vue build those assets with a content hash in the name, so etag is not usefull and the cache will be busted automatically if the content of the file change)
14 | location /assets {
15 | etag off;
16 | add_header Cache-Control "public,max-age=31536000,immutable";
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "riichi-pointer-js",
3 | "version": "0.7.4",
4 | "private": true,
5 | "license": "MIT",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "vite build",
10 | "preview": "vite preview --port 8080",
11 | "test:unit": "vitest run",
12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path .gitignore",
13 | "watch:test:unit": "vitest"
14 | },
15 | "dependencies": {
16 | "@fortawesome/fontawesome-svg-core": "^6.0.0",
17 | "@fortawesome/free-solid-svg-icons": "^6.0.0",
18 | "@fortawesome/vue-fontawesome": "^3.0.0",
19 | "dot-prop": "^9.0.0",
20 | "tiny-emitter": "^2.1.0",
21 | "vue": "^3.2.31"
22 | },
23 | "devDependencies": {
24 | "@vitejs/plugin-vue": "^5.0.0",
25 | "eslint": "^8.0.0",
26 | "eslint-config-standard": "^17.0.0",
27 | "eslint-plugin-import": "^2.0.0",
28 | "eslint-plugin-n": "^16.0.0 ",
29 | "eslint-plugin-promise": "^6.0.0",
30 | "eslint-plugin-vue": "^9.0.0",
31 | "vite": "^6.0.0",
32 | "vitest": "^2.0.0",
33 | "vite-plugin-html": "^3.0.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/package.json.note:
--------------------------------------------------------------------------------
1 | Package Current Wanted Latest Package Type URL
2 | eslint 8.57.1 8.57.1 9.11.1 devDependencies https://eslint.org
3 | eslint-plugin-n 16.6.2 16.6.2 17.10.3 devDependencies https://github.com/eslint-community/eslint-plugin-n#readme
4 | eslint-plugin-promise 6.6.0 6.6.0 7.1.0 devDependencies https://github.com/eslint-community/eslint-plugin-promise
5 |
6 | Can't update eslint to v9 for now because eslint-config-standard have a peer dependency on v8.
7 | Can't update eslint-plugin-n to v17 for now because eslint-config-standard have a peer dependency on v15 or v16.
8 | Can't update eslint-plugin-promise to v7 for now because eslint-config-standard have a peer dependency on v6.
9 |
10 | ----------------------
--------------------------------------------------------------------------------
/public/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmeraldCoder/riichi-pointer-js/2272d287df9377cf7fae585934875d9801671065/public/og-image.png
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Riichi Mahjong Pointer",
3 | "short_name": "Riichi Mahjong Pointer",
4 | "background_color": "#43a047",
5 | "theme_color": "#1b5e20"
6 | }
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
40 |
41 |
45 |
49 |
50 |
51 |
59 |
60 |
61 |
62 |
122 |
123 |
135 |
--------------------------------------------------------------------------------
/src/assets/tile-icons/sprite.2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmeraldCoder/riichi-pointer-js/2272d287df9377cf7fae585934875d9801671065/src/assets/tile-icons/sprite.2x.png
--------------------------------------------------------------------------------
/src/assets/tile-icons/sprite.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Generated and lightly adapted from https://jcthepants.github.io/Retina-CSS-Sprite-Generator/
3 | */
4 |
5 | .tile-icon {
6 | background: url('sprite.png') no-repeat top left;
7 | width: 58px;
8 | height: 78px;
9 | display: inline-block;
10 | }
11 | .tile-icon.man-1 {
12 | background-position: 0 0;
13 | }
14 | .tile-icon.man-2 {
15 | background-position: 0 -79px;
16 | }
17 | .tile-icon.man-3 {
18 | background-position: 0 -158px;
19 | }
20 | .tile-icon.man-4 {
21 | background-position: 0 -237px;
22 | }
23 | .tile-icon.man-5 {
24 | background-position: 0 -316px;
25 | }
26 | .tile-icon.man-6 {
27 | background-position: 0 -395px;
28 | }
29 | .tile-icon.man-7 {
30 | background-position: 0 -474px;
31 | }
32 | .tile-icon.man-8 {
33 | background-position: 0 -553px;
34 | }
35 | .tile-icon.man-9 {
36 | background-position: 0 -632px;
37 | }
38 | .tile-icon.pin-1 {
39 | background-position: 0 -711px;
40 | }
41 | .tile-icon.pin-2 {
42 | background-position: 0 -790px;
43 | }
44 | .tile-icon.pin-3 {
45 | background-position: 0 -869px;
46 | }
47 | .tile-icon.pin-4 {
48 | background-position: 0 -948px;
49 | }
50 | .tile-icon.pin-5 {
51 | background-position: 0 -1027px;
52 | }
53 | .tile-icon.pin-6 {
54 | background-position: 0 -1106px;
55 | }
56 | .tile-icon.pin-7 {
57 | background-position: 0 -1185px;
58 | }
59 | .tile-icon.pin-8 {
60 | background-position: 0 -1264px;
61 | }
62 | .tile-icon.pin-9 {
63 | background-position: 0 -1343px;
64 | }
65 | .tile-icon.sou-1 {
66 | background-position: 0 -1422px;
67 | }
68 | .tile-icon.sou-2 {
69 | background-position: 0 -1501px;
70 | }
71 | .tile-icon.sou-3 {
72 | background-position: 0 -1580px;
73 | }
74 | .tile-icon.sou-4 {
75 | background-position: 0 -1659px;
76 | }
77 | .tile-icon.sou-5 {
78 | background-position: 0 -1738px;
79 | }
80 | .tile-icon.sou-6 {
81 | background-position: 0 -1817px;
82 | }
83 | .tile-icon.sou-7 {
84 | background-position: 0 -1896px;
85 | }
86 | .tile-icon.sou-8 {
87 | background-position: 0 -1975px;
88 | }
89 | .tile-icon.sou-9 {
90 | background-position: 0 -2054px;
91 | }
92 | .tile-icon.hatsu {
93 | background-position: 0 -2133px;
94 | }
95 | .tile-icon.chun {
96 | background-position: 0 -2212px;
97 | }
98 | .tile-icon.haku {
99 | background: none;
100 | }
101 | .tile-icon.ton {
102 | background-position: 0 -2291px;
103 | }
104 | .tile-icon.nan {
105 | background-position: 0 -2370px;
106 | }
107 | .tile-icon.shaa {
108 | background-position: 0 -2449px;
109 | }
110 | .tile-icon.pei {
111 | background-position: 0 -2528px;
112 | }
113 |
114 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
115 | .tile-icon {
116 | background: url('sprite.2x.png') no-repeat top left;
117 | background-size: 58px 2607px;
118 | }
119 | }
--------------------------------------------------------------------------------
/src/assets/tile-icons/sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EmeraldCoder/riichi-pointer-js/2272d287df9377cf7fae585934875d9801671065/src/assets/tile-icons/sprite.png
--------------------------------------------------------------------------------
/src/components/DoraCounter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 | {{ modelValue }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
47 |
48 |
60 |
--------------------------------------------------------------------------------
/src/components/MahjongTile.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
53 |
54 |
87 |
--------------------------------------------------------------------------------
/src/components/SimpleCalculatorScoreModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
12 |
13 | {{ data.summary }}
14 |
15 |
16 |
17 | {{ data.yakuman }} yakuman
18 |
19 |
20 | {{ data.han }} han / {{ data.fu }} fu
21 |
22 |
23 | {{ data.han }} han
24 |
25 |
26 |
27 |
28 |
29 |
30 |
126 |
127 |
134 |
--------------------------------------------------------------------------------
/src/components/TileSelectionModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
23 |
24 |
25 |
26 |
78 |
79 |
89 |
--------------------------------------------------------------------------------
/src/core/combination-classes.js:
--------------------------------------------------------------------------------
1 | import { TileFactory } from './tile-classes'
2 |
3 | /**
4 | * Combination factory class
5 | * Factory design pattern to help create hand combination classes
6 | *
7 | * ex. : CombinationFactory.create('triplet', new DotTile(1))
8 | */
9 | export const CombinationFactory = {
10 | create (combinationType, firstCombinationTile) {
11 | switch (combinationType) {
12 | case 'pair':
13 | return new Pair(firstCombinationTile)
14 | case 'triplet':
15 | return new Triplet(firstCombinationTile)
16 | case 'quad':
17 | return new Quad(firstCombinationTile)
18 | case 'sequence': {
19 | const secondCombinationTile = TileFactory.create(firstCombinationTile.suit, firstCombinationTile.value + 1)
20 | const thirdCombinationTile = TileFactory.create(firstCombinationTile.suit, firstCombinationTile.value + 2)
21 | return new Sequence(firstCombinationTile, secondCombinationTile, thirdCombinationTile)
22 | }
23 | case 'orphan':
24 | return new Orphan(firstCombinationTile)
25 | default:
26 | throw new Error(`Hand Combination Factory Error: "${combinationType}" is not a supported combination type`)
27 | }
28 | }
29 | }
30 |
31 | /**
32 | * Combination base class (pair, triplet, quad, sequence)
33 | */
34 | export class Combination {
35 | constructor (tiles) {
36 | this.tiles = tiles // property that contain the tiles of the combination
37 | }
38 | }
39 |
40 | /**
41 | * Pair combination class
42 | * two identical tiles (ex. : 3,3 of bamboo tiles)
43 | *
44 | * param Tile tile
45 | */
46 | export class Pair extends Combination {
47 | constructor (tile) {
48 | super([
49 | TileFactory.create(tile.suit, tile.value),
50 | TileFactory.create(tile.suit, tile.value)
51 | ])
52 | }
53 | }
54 |
55 | /**
56 | * Triplet combination class
57 | * three identical tiles (ex. : 3,3,3 of bamboo tiles)
58 | *
59 | * param Tile tile
60 | */
61 | export class Triplet extends Combination {
62 | constructor (tile) {
63 | super([
64 | TileFactory.create(tile.suit, tile.value),
65 | TileFactory.create(tile.suit, tile.value),
66 | TileFactory.create(tile.suit, tile.value)
67 | ])
68 | }
69 | }
70 |
71 | /**
72 | * Quad combination class
73 | * four identical tiles (ex. : 3,3,3,3 of bamboo tiles)
74 | *
75 | * param Tile tile
76 | */
77 | export class Quad extends Combination {
78 | constructor (tile) {
79 | super([
80 | TileFactory.create(tile.suit, tile.value),
81 | TileFactory.create(tile.suit, tile.value),
82 | TileFactory.create(tile.suit, tile.value),
83 | TileFactory.create(tile.suit, tile.value)
84 | ])
85 | }
86 | }
87 |
88 | /**
89 | * Sequence combination class
90 | * three following numbered tile of the same suit (ex. : 2,3,4 of bamboo tiles)
91 | *
92 | * param Tile tile
93 | */
94 | export class Sequence extends Combination {
95 | constructor (tile1, tile2, tile3) {
96 | super([tile1, tile2, tile3])
97 | }
98 | }
99 |
100 | /**
101 | * Orphan combination class
102 | * one tile (only used for thirteen orphans yakuman)
103 | *
104 | * param Tile tile
105 | */
106 | export class Orphan extends Combination {
107 | constructor (tile) {
108 | super([tile])
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/core/ema/ema-fu-calculator.js:
--------------------------------------------------------------------------------
1 | import FuCalculator from '../fu-calculation/fu-calculator'
2 |
3 | import WinRule from '../fu-calculation/rules/winning-fu-rule'
4 | import WaitRule from '../fu-calculation/rules/wait-fu-rule'
5 | import TsumoRule from '../fu-calculation/rules/tsumo-fu-rule'
6 | import PairRule from '../fu-calculation/rules/pair-fu-rule'
7 | import OpenPinfuRule from '../fu-calculation/rules/open-pinfu-fu-rule'
8 | import CombinationsRule from '../fu-calculation/rules/combinations-fu-rule'
9 | import ClosedRonRule from '../fu-calculation/rules/closed-ron-fu-rule'
10 | import ChiitoitsuRule from '../fu-calculation/rules/chiitoitsu-fu-rule'
11 |
12 | import ChiitoitsuYaku from '../han-calculation/yakus/chiitoitsu-yaku'
13 | import PinfuYaku from '../han-calculation/yakus/pinfu-yaku'
14 |
15 | class EmaFuCalculator extends FuCalculator {
16 | constructor () {
17 | super([
18 | // put the chiitoitsu rule at the top because it's a fixed amount of fu
19 | new ChiitoitsuRule({ chiitoitsuYakuPattern: new ChiitoitsuYaku(), fuValue: 25 }),
20 | new WinRule(),
21 | new CombinationsRule(),
22 | new PairRule({ stackable: true }),
23 | new WaitRule(),
24 | new OpenPinfuRule(),
25 | new ClosedRonRule(),
26 | new TsumoRule({ excludedYakuPatterns: [new PinfuYaku()] })
27 | ])
28 | }
29 | }
30 |
31 | export default EmaFuCalculator
32 |
--------------------------------------------------------------------------------
/src/core/ema/ema-point-calculator.js:
--------------------------------------------------------------------------------
1 | import PointCalculator from '../point-calculation/point-calculator'
2 |
3 | class EmaPointCalculator extends PointCalculator {
4 | constructor () {
5 | super({
6 | kazoeYakumanAsSanbaiman: true,
7 | kiriageMangan: false
8 | })
9 | }
10 | }
11 |
12 | export default EmaPointCalculator
13 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/fu-calculation-context.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fu calculation context information.
3 | *
4 | * @typedef {Object} FuCalculationContext
5 | * @property {boolean} stop=false - Flag indicating if the calculator can continue to process other rules
6 | * @property {boolean} rounding=true - Flag indicating if the calculator can round up to the tens the total
7 | * @memberof FuCalculation
8 | */
9 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/fu-calculation-result.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} FuCalculationResult
3 | * @property {FuCalculation.FuInfo[]} details - Details explaining what are the fu sum in the total
4 | * @property {number} total - Total fu value of the hand. Usually round up to the tens (except in a few case like Chiitoitsu).
5 | * @memberof FuCalculation
6 | */
7 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/fu-calculator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Base class for fu calculator implementations.
3 | * This class is intended to be used internally or to create custom fu calculator.
4 | *
5 | * Contains the logic to process a list of rules to aggregate a fu total
6 | * rounded up to the tens (except in a few case like Chiitoitsu).
7 | *
8 | * Real implementation, like WrcFuCalculator, would be better if you just want to get the fu value of a hand.
9 | *
10 | * @memberof FuCalculation
11 | */
12 | class FuCalculator {
13 | /**
14 | * @param {FuCalculation.FuRule[]} rules - Set of rules to check during the fu calculation process
15 | */
16 | constructor (rules) {
17 | if (rules == null) throw new Error('rules parameter is required')
18 |
19 | this.rules = rules
20 | }
21 |
22 | /**
23 | * Calculate the fu value of a hand.
24 | *
25 | * @param {Hand} hand
26 | * @returns {FuCalculation.FuCalculationResult}
27 | */
28 | calculate (hand) {
29 | const context = { stop: false, rounding: true }
30 | const details = []
31 |
32 | for (const rule of this.rules) {
33 | const result = rule.check(hand, context)
34 | if (result) details.push(...normalizeDetails(result))
35 | if (context.stop) break
36 | }
37 |
38 | let total = details.reduce((agg, x) => agg + (x.quantity * x.fuValue), 0)
39 |
40 | if (context.rounding) total = Math.ceil(total / 10) * 10
41 |
42 | return { details, total }
43 | }
44 | }
45 |
46 | function normalizeDetails (result) {
47 | return result instanceof Array ? result : [result]
48 | }
49 |
50 | export default FuCalculator
51 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/fu-info.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Information returned by fu calculation rule about why and how much fu the calculator need to add.
3 | *
4 | * @typedef {Object} FuInfo
5 | * @property {string} key - Key to identify why the fu are added
6 | * @property {number} fuValue - Amount of fu added
7 | * @property {number} quantity - Number of time those fu need to be added
8 | * @memberof FuCalculation
9 | */
10 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/fu-rule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fu rule interface
3 | *
4 | * @interface FuRule
5 | * @memberof FuCalculation
6 | */
7 |
8 | /**
9 | * Function called by the fu calculator to know what fu are allowed according to this rule.
10 | *
11 | * @function
12 | * @name FuCalculation.FuRule#check
13 | * @param {Hand} hand - Hand to apply the rule onto
14 | * @param {FuCalculation.FuCalculationContext} context - Context of the fu calculation process
15 | * @returns {(undefined|FuCalculation.FuInfo|FuCalculation.FuInfo[])}
16 | */
17 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @namespace FuCalculation
3 | */
4 |
5 | export { default as ChiitoitsuFuRule } from './rules/chiitoitsu-fu-rule'
6 | export { default as ClosedRonFuRule } from './rules/closed-ron-fu-rule'
7 | export { default as CombinationsFuRule } from './rules/combinations-fu-rule'
8 | export { default as OpenPinfuFuRule } from './rules/open-pinfu-fu-rule'
9 | export { default as PairFuRule } from './rules/pair-fu-rule'
10 | export { default as TsumoFuRule } from './rules/tsumo-fu-rule'
11 | export { default as WaitFuRule } from './rules/wait-fu-rule'
12 | export { default as WinningFuRule } from './rules/winning-fu-rule'
13 |
14 | export { default as FuCalculator } from './fu-calculator'
15 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/rules/chiitoitsu-fu-rule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fu calculation rule that will attribute a fixed amount of fu (usually 25) for hands with chiitoitsu (seven pairs).
3 | *
4 | * As an exception, the fu value of the hand is not rounded up to the tens because it is worth of fixed amount.
5 | *
6 | * Some rules say seven pairs has 50 fu and one han, especially in the Kansai region.
7 | * https://en.wikipedia.org/wiki/Japanese_Mahjong_scoring_rules#Steps_of_calculation
8 | *
9 | * @implements FuCalculation.FuRule
10 | * @memberof FuCalculation
11 | */
12 | class ChiitoitsuFuRule {
13 | /**
14 | * @param {Object} options - Configuration options for the rule
15 | * @param {YakuPattern} options.chiitoitsuYakuPattern - Yaku pattern to validate the hand shape
16 | * @param {number} [options.fuValue=25] - Amount of fu to add if the rule is valid
17 | */
18 | constructor (options) {
19 | if (options?.chiitoitsuYakuPattern == null) throw new Error('chiitoitsuYakuPattern is required')
20 |
21 | this.chiitoitsuYakuPattern = options.chiitoitsuYakuPattern
22 | this.fuValue = options?.fuValue ?? 25
23 | }
24 |
25 | /** @override */
26 | check (hand, context) {
27 | if (this.chiitoitsuYakuPattern.check(hand)) {
28 | // tell the calculator to stop processing the other rules
29 | // and to not round up the total because this hand is always worth a fixed amount of fu.
30 | context.stop = true
31 | context.rounding = false
32 | return { key: 'chiitoitsu', fuValue: this.fuValue, quantity: 1 }
33 | }
34 | }
35 | }
36 |
37 | export default ChiitoitsuFuRule
38 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/rules/closed-ron-fu-rule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fu calculation rule that will attribute 10 fu for concealed hands won by ron.
3 | *
4 | * @implements FuCalculation.FuRule
5 | * @memberof FuCalculation
6 | */
7 | class ClosedRonFuRule {
8 | /** @override */
9 | check ({ isOpen, winningType }) {
10 | if (!isOpen && winningType === 'ron') {
11 | return { key: 'closed ron', fuValue: 10, quantity: 1 }
12 | }
13 | }
14 | }
15 |
16 | export default ClosedRonFuRule
17 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/rules/combinations-fu-rule.js:
--------------------------------------------------------------------------------
1 | import { HonorTile } from './../../tile-classes'
2 | import { Triplet, Quad } from './../../combination-classes'
3 |
4 | /**
5 | * Fu calculation rule that will attribute fu according to the hand's combinations.
6 | *
7 | *
8 | * Combination Simples Terminal / Honor
9 | * Minkou (Open Triplet) 2 4
10 | * Ankou (Concealed Triplet) 4 8
11 | * Minkan (Open Quad) 8 16
12 | * Ankan (Concealed Quad) 16 32
13 | *
14 | *
15 | * @implements FuCalculation.FuRule
16 | * @memberof FuCalculation
17 | */
18 | class CombinationsFuRule {
19 | /** @override */
20 | check (hand) {
21 | const result = []
22 |
23 | const counts = countCombinationType(hand)
24 |
25 | if (counts.minkouSimple > 0) result.push({ key: 'minkou simple', fuValue: 2, quantity: counts.minkouSimple })
26 | if (counts.minkouNonSimple > 0) result.push({ key: 'minkou non simple', fuValue: 4, quantity: counts.minkouNonSimple })
27 | if (counts.minkanSimple > 0) result.push({ key: 'minkan simple', fuValue: 8, quantity: counts.minkanSimple })
28 | if (counts.minkanNonSimple > 0) result.push({ key: 'minkan non simple', fuValue: 16, quantity: counts.minkanNonSimple })
29 | if (counts.ankouSimple > 0) result.push({ key: 'ankou simple', fuValue: 4, quantity: counts.ankouSimple })
30 | if (counts.ankouNonSimple > 0) result.push({ key: 'ankou non simple', fuValue: 8, quantity: counts.ankouNonSimple })
31 | if (counts.ankanSimple > 0) result.push({ key: 'ankan simple', fuValue: 16, quantity: counts.ankanSimple })
32 | if (counts.ankanNonSimple > 0) result.push({ key: 'ankan non simple', fuValue: 32, quantity: counts.ankanNonSimple })
33 |
34 | if (result.length > 0) return result
35 | }
36 | }
37 |
38 | function isSimpleTile (tile) {
39 | return !(tile instanceof HonorTile) && !tile.isTerminal()
40 | }
41 |
42 | function countCombinationType (hand) {
43 | let counts = {
44 | minkouSimple: 0,
45 | minkouNonSimple: 0,
46 | ankouSimple: 0,
47 | ankouNonSimple: 0,
48 | minkanSimple: 0,
49 | minkanNonSimple: 0,
50 | ankanSimple: 0,
51 | ankanNonSimple: 0
52 | }
53 |
54 | counts = hand.concealedCombinations.reduce((agg, combination, combinationIndex) => {
55 | if (combination instanceof Triplet) {
56 | if (hand.winningType === 'ron' && hand.winningCombinationIndex === combinationIndex) {
57 | if (isSimpleTile(combination.tiles[0])) {
58 | agg.minkouSimple++
59 | } else {
60 | agg.minkouNonSimple++
61 | }
62 | } else {
63 | if (isSimpleTile(combination.tiles[0])) {
64 | agg.ankouSimple++
65 | } else {
66 | agg.ankouNonSimple++
67 | }
68 | }
69 | } else if (combination instanceof Quad) {
70 | if (isSimpleTile(combination.tiles[0])) {
71 | agg.ankanSimple++
72 | } else {
73 | agg.ankanNonSimple++
74 | }
75 | }
76 |
77 | return agg
78 | }, counts)
79 |
80 | return hand.openCombinations.reduce((agg, combination) => {
81 | if (combination instanceof Triplet) {
82 | if (isSimpleTile(combination.tiles[0])) {
83 | agg.minkouSimple++
84 | } else {
85 | agg.minkouNonSimple++
86 | }
87 | } else if (combination instanceof Quad) {
88 | if (isSimpleTile(combination.tiles[0])) {
89 | agg.minkanSimple++
90 | } else {
91 | agg.minkanNonSimple++
92 | }
93 | }
94 |
95 | return agg
96 | }, counts)
97 | }
98 |
99 | export default CombinationsFuRule
100 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/rules/open-pinfu-fu-rule.js:
--------------------------------------------------------------------------------
1 | import { Pair, Sequence } from './../../combination-classes'
2 | import { DragonTile } from './../../tile-classes'
3 | import isTankiWait from './../../waits/is-tanki-wait'
4 | import isKanchanWait from './../../waits/is-kanchan-wait'
5 | import isPenchanWait from './../../waits/is-penchan-wait'
6 |
7 | /**
8 | * Fu calculation rule that will attribute fu if the hand was open and won by ron without any fu for the pair, the combinations and the wait.
9 | *
10 | * @implements FuCalculation.FuRule
11 | * @memberof FuCalculation
12 | */
13 | class OpenPinfuFuRule {
14 | /** @override */
15 | check (hand) {
16 | if (hand.winningType === 'ron' && hand.isOpen) {
17 | const chiis = hand.combinations.filter(x => x instanceof Sequence)
18 | const pairs = hand.concealedCombinations.filter(x => x instanceof Pair)
19 |
20 | if (
21 | chiis.length === 4 &&
22 | pairs.length === 1 &&
23 |
24 | !(pairs[0].tiles[0] instanceof DragonTile) &&
25 | pairs[0].tiles[0].value !== hand.roundWind &&
26 | pairs[0].tiles[0].value !== hand.seatWind &&
27 |
28 | !isTankiWait(hand) &&
29 | !isKanchanWait(hand) &&
30 | !isPenchanWait(hand)
31 | ) {
32 | return { key: 'open pinfu', fuValue: 10, quantity: 1 }
33 | }
34 | }
35 | }
36 | }
37 |
38 | export default OpenPinfuFuRule
39 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/rules/pair-fu-rule.js:
--------------------------------------------------------------------------------
1 | import { Pair } from './../../combination-classes'
2 | import { DragonTile } from './../../tile-classes'
3 |
4 | /**
5 | * Fu calculation rule that will attribute fu according to the hand's pair.
6 | *
7 | * If the hand's pair is of tiles that would score yakuhai in a koutsu, then this scores 2 fu.
8 | * If the pair doubles up as both the round wind and the seat wind, it may score 2 fu, or 4 fu.
9 | * That is dependent on which scoring rule is used for this case.
10 | * http://arcturus.su/wiki/Fu
11 | *
12 | * @implements FuCalculation.FuRule
13 | * @memberof FuCalculation
14 | */
15 | class PairFuRule {
16 | /**
17 | * @param {Object} options - Configuration options for the rule
18 | * @param {boolean} [options.stackable=true] - Flag indicating if we can stack the fu for the round wind and seat wind
19 | */
20 | constructor (options) {
21 | this.stackable = options?.stackable ?? true
22 | }
23 |
24 | /** @override */
25 | check ({ concealedCombinations, roundWind, seatWind }) {
26 | const pairs = concealedCombinations.filter(x => x instanceof Pair)
27 |
28 | if (pairs.length !== 1) return
29 |
30 | const tile = pairs[0].tiles[0]
31 |
32 | if (
33 | tile instanceof DragonTile ||
34 | tile.value === roundWind ||
35 | tile.value === seatWind
36 | ) {
37 | let quantity = 1
38 |
39 | if (this.stackable && tile.value === roundWind && tile.value === seatWind) {
40 | quantity++
41 | }
42 |
43 | return { key: 'pair', fuValue: 2, quantity }
44 | }
45 | }
46 | }
47 |
48 | export default PairFuRule
49 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/rules/tsumo-fu-rule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fu calculation rule that will attribute 2 fu if the hand was won by tsumo.
3 | *
4 | * Depending of the ruleset, some hands with certain yaku are not eligible. In most ruleset, this is the case for hands with a pinfu yaku.
5 | *
6 | * There is also some ruleset that exclude hands won with Rinshan Kaihou.
7 | *
8 | * @implements FuCalculation.FuRule
9 | * @memberof FuCalculation
10 | */
11 | class TsumoFuRule {
12 | /**
13 | * @param {Object} options - Configuration options for the rule
14 | * @param {YakuPattern[]} [options.excludedYakuPatterns=[]] - Yaku patterns excluded from getting the fu from this rule.
15 | * (usually Pinfu, but can also be Rinshan Kaihou)
16 | */
17 | constructor (options) {
18 | this.excludedYakuPatterns = options?.excludedYakuPatterns ?? []
19 | }
20 |
21 | /** @override */
22 | check (hand) {
23 | let excluded = false
24 |
25 | for (const yakuPattern of this.excludedYakuPatterns) {
26 | if (yakuPattern.check(hand)) {
27 | excluded = true
28 | break
29 | }
30 | }
31 |
32 | if (!excluded && hand.winningType === 'tsumo') {
33 | return { key: 'tsumo', fuValue: 2, quantity: 1 }
34 | }
35 | }
36 | }
37 |
38 | export default TsumoFuRule
39 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/rules/wait-fu-rule.js:
--------------------------------------------------------------------------------
1 | import isTankiWait from './../../waits/is-tanki-wait'
2 | import isKanchanWait from './../../waits/is-kanchan-wait'
3 | import isPenchanWait from './../../waits/is-penchan-wait'
4 |
5 | /**
6 | * Fu calculation rule that will attribute fu according to the hand's wait.
7 | *
8 | * If the hand was waiting on only one tile, single wait (tanki), closed wait (kanchan) or edge wait (penchan), it is worth 2 fu.
9 | *
10 | * Other waits are worth nothing.
11 | *
12 | * @implements FuCalculation.FuRule
13 | * @memberof FuCalculation
14 | */
15 | class WaitFuRule {
16 | /** @override */
17 | check (hand) {
18 | if (isTankiWait(hand) || isKanchanWait(hand) || isPenchanWait(hand)) {
19 | return { key: 'wait', fuValue: 2, quantity: 1 }
20 | }
21 | }
22 | }
23 |
24 | export default WaitFuRule
25 |
--------------------------------------------------------------------------------
/src/core/fu-calculation/rules/winning-fu-rule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fu calculation rule that will attribute a default value of 20 fu for winning a hand.
3 | *
4 | * @implements FuCalculation.FuRule
5 | * @memberof FuCalculation
6 | */
7 | class WinningFuRule {
8 | /** @override */
9 | check () {
10 | return { key: 'win', fuValue: 20, quantity: 1 }
11 | }
12 | }
13 |
14 | export default WinningFuRule
15 |
--------------------------------------------------------------------------------
/src/core/han-calculation/han-calculation-result.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} HanCalculationResult
3 | * @property {HanCalculation.HanInfo[]} details - Details explaining what are the han eligible for the hand
4 | * @property {number} han - Total han value of the hand. (null if it's a yakuman hand)
5 | * @property {number} yakuman - Total yakuman value of the hand. (null if it's not a yakuman hand)
6 | * @memberof HanCalculation
7 | */
8 |
--------------------------------------------------------------------------------
/src/core/han-calculation/han-calculator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Base class for han calculator implementations.
3 | * This class is intended to be used internally or to create custom han calculator.
4 | *
5 | * Contains the logic to process a list of yaku to aggregate a han or yakuman total
6 | *
7 | * Real implementation, like WrcHanCalculator, would be better if you just want to get the han or yakuman value of a hand.
8 | *
9 | * @memberof HanCalculation
10 | */
11 | class HanCalculator {
12 | /**
13 | * @param {HanCalculation.Yaku[]} yakus - Set of yaku to check during the han calculation process
14 | */
15 | constructor (yakus, options) {
16 | if (yakus == null) throw new Error('yakus parameter is required')
17 |
18 | this.yakus = yakus
19 | this.stackableYakuman = options?.stackableYakuman ?? false
20 | }
21 |
22 | /**
23 | * Calculate the han value of a hand.
24 | *
25 | * @param {Hand} hand
26 | * @returns {HanCalculation.HanCalculationResult}
27 | */
28 | calculate (hand) {
29 | const details = []
30 |
31 | // process the hand for each yaku
32 |
33 | for (const yaku of this.yakus) {
34 | const result = yaku.check(hand)
35 | if (result) details.push(...normalizeDetails(result))
36 | }
37 |
38 | // sum the yakuman value of the hand and return early if it's higher than 0
39 | // because we don't need the han value if it's a yakuman hand
40 |
41 | const yakumanDetails = details.filter(x => x.yakumanValue != null && x.yakumanValue > 0)
42 |
43 | if (yakumanDetails.length > 0) {
44 | let yakuman = 0
45 |
46 | if (this.stackableYakuman) {
47 | yakuman = sum(yakumanDetails.map(x => x.yakumanValue))
48 | } else {
49 | // take the value of the yakuman with the highest value
50 | yakuman = Math.max(...yakumanDetails.map(x => x.yakumanValue))
51 | }
52 |
53 | return { details: details.filter(x => x.yakumanValue != null && x.yakumanValue > 0), han: null, yakuman }
54 | }
55 |
56 | // if it's not a yakuman hand
57 | // sum the han value of the hand
58 |
59 | let han = sum(details.map(x => x.hanValue))
60 |
61 | // add the number of dora to the han value of the hand only if the hand have
62 | // already some valid yaku
63 |
64 | if (han > 0 && hand.nbDora > 0) {
65 | han += hand.nbDora
66 | details.push({ key: 'dora', hanValue: hand.nbDora, yakumanValue: 0 })
67 | }
68 |
69 | return { details, han: han > 0 ? han : null, yakuman: null }
70 | }
71 | }
72 |
73 | function sum (array) {
74 | return array.reduce((agg, x) => {
75 | if (x != null) agg += x
76 | return agg
77 | }, 0)
78 | }
79 |
80 | function normalizeDetails (result) {
81 | return result instanceof Array ? result : [result]
82 | }
83 |
84 | export default HanCalculator
85 |
--------------------------------------------------------------------------------
/src/core/han-calculation/han-info.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Information returned by yaku about why and how much han or yakuman the calculator need to add.
3 | *
4 | * @typedef {Object} HanInfo
5 | * @property {string} key - Key to identify why the han are added
6 | * @property {number} hanValue - Amount of han added
7 | * @property {number} yakumanValue - Amount of yakuman added
8 | * @memberof HanCalculation
9 | */
10 |
--------------------------------------------------------------------------------
/src/core/han-calculation/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @namespace HanCalculation
3 | */
4 |
5 | export { default as ChankanYaku } from './yakus/chankan-yaku'
6 | export { default as ChantaYaku } from './yakus/chanta-yaku'
7 | export { default as ChiihouYaku } from './yakus/chiihou-yaku'
8 | export { default as ChiitoitsuYaku } from './yakus/chiitoitsu-yaku'
9 | export { default as ChinitsuYaku } from './yakus/chinitsu-yaku'
10 | export { default as ChinroutouYaku } from './yakus/chinroutou-yaku'
11 | export { default as ChuurenPoutouYaku } from './yakus/chuuren-poutou-yaku'
12 | export { default as DaisangenYaku } from './yakus/daisangen-yaku'
13 | export { default as DaisuushiiYaku } from './yakus/daisuushii-yaku'
14 | export { default as DoubleRiichiYaku } from './yakus/double-riichi-yaku'
15 | export { default as HaiteiRaoyueYaku } from './yakus/haitei-raoyue-yaku'
16 | export { default as HonitsuYaku } from './yakus/honitsu-yaku'
17 | export { default as HonroutouYaku } from './yakus/honroutou-yaku'
18 | export { default as HouteiRaoyuiYaku } from './yakus/houtei-raoyui-yaku'
19 | export { default as IipeikouYaku } from './yakus/iipeikou-yaku'
20 | export { default as IppatsuYaku } from './yakus/ippatsu-yaku'
21 | export { default as IttsuuYaku } from './yakus/ittsuu-yaku'
22 | export { default as JunchanYaku } from './yakus/junchan-yaku'
23 | export { default as KokushiMusouYaku } from './yakus/kokushi-musou-yaku'
24 | export { default as MenzenTsumoYaku } from './yakus/menzen-tsumo-yaku'
25 | export { default as OpenRiichiYaku } from './yakus/open-riichi-yaku'
26 | export { default as PinfuYaku } from './yakus/pinfu-yaku'
27 | export { default as RenhouYaku } from './yakus/renhou-yaku'
28 | export { default as RiichiYaku } from './yakus/riichi-yaku'
29 | export { default as RinshanKaihouYaku } from './yakus/rinshan-kaihou-yaku'
30 | export { default as RyanpeikouYaku } from './yakus/ryanpeikou-yaku'
31 | export { default as RyuuiisouYaku } from './yakus/ryuuiisou-yaku'
32 | export { default as SanankouYaku } from './yakus/sanankou-yaku'
33 | export { default as SankantsuYaku } from './yakus/sankantsu-yaku'
34 | export { default as SanshokuDoujunYaku } from './yakus/sanshoku-doujun-yaku'
35 | export { default as SanshokuDoukouYaku } from './yakus/sanshoku-doukou-yaku'
36 | export { default as ShousangenYaku } from './yakus/shousangen-yaku'
37 | export { default as ShousuushiiYaku } from './yakus/shousuushii-yaku'
38 | export { default as SuuankouYaku } from './yakus/suuankou-yaku'
39 | export { default as SuukantsuYaku } from './yakus/suukantsu-yaku'
40 | export { default as TanyaoYaku } from './yakus/tanyao-yaku'
41 | export { default as TenhouYaku } from './yakus/tenhou-yaku'
42 | export { default as ToitoiYaku } from './yakus/toitoi-yaku'
43 | export { default as TsuuiisouYaku } from './yakus/tsuuiisou-yaku'
44 | export { default as YakuhaiYaku } from './yakus/yakuhai-yaku'
45 |
46 | export { default as HanCalculator } from './han-calculator'
47 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Yaku interface
3 | *
4 | * @interface Yaku
5 | * @memberof HanCalculation
6 | */
7 |
8 | /**
9 | * Function called by the han calculator to know what han are allowed according to this yaku.
10 | *
11 | * @function
12 | * @name HanCalculation.Yaku#check
13 | * @param {Hand} hand - Hand to check the yaku onto
14 | * @returns {(undefined|HanCalculation.HanInfo|HanCalculation.HanInfo[])}
15 | */
16 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/chankan-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Chankan (Robbing the kan) yaku pattern
3 | *
4 | * Winning on off a tile used to extend a kong.
5 | *
6 | * Must be concealed: no
7 | * Han: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class ChankanYaku {
13 | /** @override */
14 | check ({ yakus }) {
15 | if (yakus.includes('chankan')) return { key: 'chankan', hanValue: 1, yakumanValue: 0 }
16 | }
17 | }
18 |
19 | export default ChankanYaku
20 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/chanta-yaku.js:
--------------------------------------------------------------------------------
1 | import { Sequence } from './../../combination-classes'
2 | import { HonorTile } from './../../tile-classes'
3 |
4 | /**
5 | * Chanta (outside hand) yaku pattern
6 | *
7 | * A hand where all sets contain a terminal or honor tile, and at least one of the sets is a sequence.
8 | *
9 | * Must be concealed: no
10 | * Han: 2 (concealed) / 1 (open)
11 | *
12 | * @implements HanCalculation.Yaku
13 | * @memberof HanCalculation
14 | */
15 | class ChantaYaku {
16 | /** @override */
17 | check ({ combinations, isOpen }) {
18 | let nbChii = 0
19 | let nbHonorTile = 0
20 |
21 | for (const combination of combinations) {
22 | if (combination instanceof Sequence) nbChii++
23 |
24 | const nbTerminalOrHonor = combination.tiles.reduce((agg, tile) => {
25 | if (tile instanceof HonorTile) {
26 | agg++
27 | nbHonorTile++
28 | } else if (tile.isTerminal()) {
29 | agg++
30 | }
31 | return agg
32 | }, 0)
33 |
34 | if (nbTerminalOrHonor === 0) return
35 | }
36 |
37 | // without sequence it would be considered a honroutou or chinroutou
38 | // without honor tile it would be considered a junchan
39 |
40 | if (nbChii && nbHonorTile) {
41 | return { key: 'chanta', hanValue: isOpen ? 1 : 2, yakumanValue: 0 }
42 | }
43 | }
44 | }
45 |
46 | export default ChantaYaku
47 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/chiihou-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Chiihou (Hand of earth)
3 | *
4 | * A hand won by a non-dealer on his first draw when no open-meld declaration has been made
5 | *
6 | * Must be concealed: yes
7 | * Yakuman: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class ChiihouYaku {
13 | /** @override */
14 | check ({ yakus }) {
15 | if (yakus.includes('chiihou')) return { key: 'chiihou', hanValue: 0, yakumanValue: 1 }
16 | }
17 | }
18 |
19 | export default ChiihouYaku
20 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/chiitoitsu-yaku.js:
--------------------------------------------------------------------------------
1 | import { Pair } from './../../combination-classes'
2 |
3 | /**
4 | * Chiitoitsu (seven pairs) yaku pattern
5 | *
6 | * A hand consisting of seven pairs
7 | *
8 | * Must be concealed: yes
9 | * Han: 2
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class ChiitoitsuYaku {
15 | /** @override */
16 | check ({ concealedCombinations }) {
17 | const nbPair = concealedCombinations.reduce((agg, combination) => {
18 | if (combination instanceof Pair) agg++
19 | return agg
20 | }, 0)
21 |
22 | if (nbPair === 7) return { key: 'chiitoitsu', hanValue: 2, yakumanValue: 0 }
23 | }
24 | }
25 |
26 | export default ChiitoitsuYaku
27 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/chinitsu-yaku.js:
--------------------------------------------------------------------------------
1 | import { HonorTile } from './../../tile-classes'
2 |
3 | /**
4 | * Chinitsu (full flush) yaku pattern
5 | *
6 | * A hand with tiles from only one suit
7 | *
8 | * Must be concealed: no
9 | * Han: 6 (concealed) / 5 (open)
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class ChinitsuYaku {
15 | /** @override */
16 | check ({ combinations, isOpen }) {
17 | let suit = null
18 |
19 | for (const combination of combinations) {
20 | for (const tile of combination.tiles) {
21 | if (tile instanceof HonorTile) return
22 |
23 | if (suit == null) {
24 | suit = tile.suit
25 | } else if (suit !== tile.suit) {
26 | return
27 | }
28 | }
29 | }
30 |
31 | return { key: 'chinitsu', hanValue: isOpen ? 5 : 6, yakumanValue: 0 }
32 | }
33 | }
34 |
35 | export default ChinitsuYaku
36 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/chinroutou-yaku.js:
--------------------------------------------------------------------------------
1 | import { NumberedTile } from './../../tile-classes'
2 |
3 | /**
4 | * Chin Routou (All Terminals)
5 | *
6 | * A hand with only terminal tiles (1 and 9)
7 | *
8 | * Must be concealed: no
9 | * Yakuman: 1
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class ChinroutouYaku {
15 | /** @override */
16 | check ({ combinations }) {
17 | for (const combination of combinations) {
18 | for (const tile of combination.tiles) {
19 | if (!(tile instanceof NumberedTile && tile.isTerminal())) {
20 | return
21 | }
22 | }
23 | }
24 |
25 | return { key: 'chinroutou', hanValue: 0, yakumanValue: 1 }
26 | }
27 | }
28 |
29 | export default ChinroutouYaku
30 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/chuuren-poutou-yaku.js:
--------------------------------------------------------------------------------
1 | import { NumberedTile } from './../../tile-classes'
2 |
3 | /**
4 | * Chuuren Poutou (Nine Gates)
5 | *
6 | * A hand with (1-1-1-2-3-4-5-6-7-8-9-9-9) of the same suit and an other tile of the same suit
7 | * Worth two yakuman if the hand was waiting on nine different tiles (Junsei Chuuren Poutou)
8 | *
9 | * Must be concealed: yes
10 | * Yakuman: 1 / 2 (Waiting on nine different tiles)
11 | *
12 | * @implements HanCalculation.Yaku
13 | * @memberof HanCalculation
14 | */
15 | class ChuurenPoutouYaku {
16 | constructor (options) {
17 | this.allowDoubleYakuman = options?.allowDoubleYakuman ?? false
18 | }
19 |
20 | /** @override */
21 | check ({ concealedCombinations, winningCombinationIndex, winningTileIndex }) {
22 | let suit = null
23 |
24 | const numbers = concealedCombinations.reduce((numbers, combination) => {
25 | combination.tiles.forEach(tile => {
26 | if (suit == null) suit = tile.suit
27 |
28 | if (tile.suit === suit && tile instanceof NumberedTile) {
29 | numbers[tile.number]++
30 | }
31 | })
32 | return numbers
33 | }, { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0 })
34 |
35 | if (!containsAllChuurenPoutouNumbers(numbers)) return
36 |
37 | numbers[concealedCombinations[winningCombinationIndex].tiles[winningTileIndex].number]--
38 |
39 | const yakumanValue = containsAllChuurenPoutouNumbers(numbers) && this.allowDoubleYakuman ? 2 : 1
40 |
41 | return { key: 'chuuren poutou', hanValue: 0, yakumanValue }
42 | }
43 | }
44 |
45 | function containsAllChuurenPoutouNumbers (numbers) {
46 | return numbers[1] > 2 && numbers[9] > 2 && numbers[2] && numbers[3] && numbers[4] && numbers[5] && numbers[6] && numbers[7] && numbers[8]
47 | }
48 |
49 | export default ChuurenPoutouYaku
50 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/daisangen-yaku.js:
--------------------------------------------------------------------------------
1 | import { Triplet, Quad } from './../../combination-classes'
2 | import { DragonTile } from './../../tile-classes'
3 |
4 | /**
5 | * Dai Sangen (Big Three Dragons)
6 | *
7 | * A hand with a triplet or quad of each type of dragon tile
8 | *
9 | * Must be concealed: no
10 | * Yakuman: 1
11 | *
12 | * @implements HanCalculation.Yaku
13 | * @memberof HanCalculation
14 | */
15 | class DaisangenYaku {
16 | /** @override */
17 | check ({ combinations }) {
18 | const nbOfDragonPonOrKan = combinations.filter(x => (x instanceof Triplet || x instanceof Quad) && x.tiles[0] instanceof DragonTile).length
19 |
20 | if (nbOfDragonPonOrKan === 3) return { key: 'daisangen', hanValue: 0, yakumanValue: 1 }
21 | }
22 | }
23 |
24 | export default DaisangenYaku
25 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/daisuushii-yaku.js:
--------------------------------------------------------------------------------
1 | import { Triplet, Quad } from './../../combination-classes'
2 | import { WindTile } from './../../tile-classes'
3 |
4 | /**
5 | * Dai Suushii (Big Four Winds)
6 | *
7 | * A hand with four triplet/quad of winds
8 | *
9 | * Must be concealed: no
10 | * Yakuman: 2
11 | *
12 | * @implements HanCalculation.Yaku
13 | * @memberof HanCalculation
14 | */
15 | class DaisuushiiYaku {
16 | constructor (options) {
17 | this.allowDoubleYakuman = options?.allowDoubleYakuman ?? false
18 | }
19 |
20 | /** @override */
21 | check ({ combinations }) {
22 | const nbOfWindPonOrKan = combinations.filter(x => (x instanceof Triplet || x instanceof Quad) && x.tiles[0] instanceof WindTile).length
23 |
24 | if (nbOfWindPonOrKan === 4) return { key: 'daisuushii', hanValue: 0, yakumanValue: this.allowDoubleYakuman ? 2 : 1 }
25 | }
26 | }
27 |
28 | export default DaisuushiiYaku
29 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/double-riichi-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Double Riichi yaku pattern
3 | *
4 | * Declaring riichi within the first uninterrupted go around.
5 | *
6 | * Must be concealed: yes
7 | * Han: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class DoubleRiichiYaku {
13 | /** @override */
14 | check ({ yakus }) {
15 | if (yakus.includes('double riichi')) return { key: 'double riichi', hanValue: 1, yakumanValue: 0 }
16 | }
17 | }
18 |
19 | export default DoubleRiichiYaku
20 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/haitei-raoyue-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Haitei Raoyue (Last Tile Draw) yaku pattern
3 | *
4 | * Winning on the very last tile
5 | *
6 | * Must be concealed: no
7 | * Han: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class HaiteiRaoyueYaku {
13 | /** @override */
14 | check ({ yakus }) {
15 | if (yakus.includes('haitei raoyue')) return { key: 'haitei raoyue', hanValue: 1, yakumanValue: 0 }
16 | }
17 | }
18 |
19 | export default HaiteiRaoyueYaku
20 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/honitsu-yaku.js:
--------------------------------------------------------------------------------
1 | import { HonorTile } from './../../tile-classes'
2 |
3 | /**
4 | * Honitsu (half flush) yaku pattern
5 | *
6 | * A hand with tiles from only one suit plus honor tiles
7 | *
8 | * Must be concealed: no
9 | * Han: 3 (concealed) / 2 (open)
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class HonitsuYaku {
15 | /** @override */
16 | check ({ combinations, isOpen }) {
17 | let nbHonorTile = 0
18 | let suit = null
19 |
20 | for (const combination of combinations) {
21 | for (const tile of combination.tiles) {
22 | if (tile instanceof HonorTile) {
23 | nbHonorTile++
24 | } else if (suit == null) {
25 | suit = tile.suit
26 | } else if (suit !== tile.suit) {
27 | return
28 | }
29 | }
30 | }
31 |
32 | if (nbHonorTile === 0) return // would be a chinitsu (full flush) instead
33 |
34 | return { key: 'honitsu', hanValue: isOpen ? 2 : 3, yakumanValue: 0 }
35 | }
36 | }
37 |
38 | export default HonitsuYaku
39 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/honroutou-yaku.js:
--------------------------------------------------------------------------------
1 | import { HonorTile } from './../../tile-classes'
2 |
3 | /**
4 | * Honroutou (all terminals & honors) yaku pattern
5 | *
6 | * A hand consisting of only terminals and honors
7 | *
8 | * Must be concealed: no
9 | * Han: 2
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class HonroutouYaku {
15 | /** @override */
16 | check ({ combinations }) {
17 | for (const combination of combinations) {
18 | for (const tile of combination.tiles) {
19 | if (!(tile instanceof HonorTile || tile.isTerminal())) return
20 | }
21 | }
22 |
23 | return { key: 'honroutou', hanValue: 2, yakumanValue: 0 }
24 | }
25 | }
26 |
27 | export default HonroutouYaku
28 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/houtei-raoyui-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Houtei Raoyui (Last Tile Discard) yaku pattern
3 | *
4 | * Winning on the very last discard
5 | *
6 | * Must be concealed: no
7 | * Han: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class HouteiRaoyuiYaku {
13 | /** @override */
14 | check ({ yakus }) {
15 | if (yakus.includes('houtei raoyui')) return { key: 'houtei raoyui', hanValue: 1, yakumanValue: 0 }
16 | }
17 | }
18 |
19 | export default HouteiRaoyuiYaku
20 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/iipeikou-yaku.js:
--------------------------------------------------------------------------------
1 | import { Sequence } from './../../combination-classes'
2 |
3 | /**
4 | * Iipeikou (pure double sequence) yaku pattern
5 | *
6 | * Two chiis of the same value and suit
7 | *
8 | * Must be concealed: yes
9 | * Han: 1
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class IipeikouYaku {
15 | /** @override */
16 | check ({ combinations, isOpen }) {
17 | if (isOpen) return
18 |
19 | const storedChiis = []
20 | let numberOfIdenticalChiis = 0
21 |
22 | for (const combination of combinations) {
23 | if (combination instanceof Sequence) {
24 | const identicalChiis = storedChiis.filter(x => x.tiles[0].suit === combination.tiles[0].suit && x.tiles[0].value === combination.tiles[0].value)
25 | if (identicalChiis.length > 0) numberOfIdenticalChiis++
26 | storedChiis.push(combination)
27 | }
28 | }
29 |
30 | if (numberOfIdenticalChiis === 1) return { key: 'iipeikou', hanValue: 1, yakumanValue: 0 }
31 | }
32 | }
33 |
34 | export default IipeikouYaku
35 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/ippatsu-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Ippatsu (One Shot) yaku pattern
3 | *
4 | * Winning within the first uninterrupted go around after declaring riichi.
5 | *
6 | * Must be concealed: yes
7 | * Han: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class IppatsuYaku {
13 | /** @override */
14 | check ({ yakus }) {
15 | if (yakus.includes('ippatsu')) return { key: 'ippatsu', hanValue: 1, yakumanValue: 0 }
16 | }
17 | }
18 |
19 | export default IppatsuYaku
20 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/ittsuu-yaku.js:
--------------------------------------------------------------------------------
1 | import { Sequence } from './../../combination-classes'
2 |
3 | /**
4 | * Ittsu or Ikkitsuukan (pure straight) yaku pattern
5 | *
6 | * Three consecutive chiis (1-9) in the same suit
7 | *
8 | * Must be concealed: no
9 | * Han: 2 (concealed) / 1 (open)
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class IttsuuYaku {
15 | /** @override */
16 | check ({ combinations, isOpen }) {
17 | const chiiCount = {}
18 |
19 | for (const combination of combinations) {
20 | if (combination instanceof Sequence) {
21 | const tile = combination.tiles[0]
22 |
23 | if (chiiCount[tile.suit] == null) {
24 | chiiCount[tile.suit] = { 1: 0, 4: 0, 7: 0 }
25 | }
26 |
27 | const count = chiiCount[tile.suit]
28 |
29 | count[tile.number]++
30 |
31 | if (count[1] && count[4] && count[7]) {
32 | return { key: 'ittsuu', hanValue: isOpen ? 1 : 2, yakumanValue: 0 }
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
39 | export default IttsuuYaku
40 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/junchan-yaku.js:
--------------------------------------------------------------------------------
1 | import { Sequence } from './../../combination-classes'
2 | import { NumberedTile } from './../../tile-classes'
3 |
4 | /**
5 | * Junchan or Junchan Taiyai or Junchan Tayao (terminals in all sets) yaku pattern
6 | *
7 | * A hand with at least one sequence and where all sets and the pair contains terminals
8 | *
9 | * Must be concealed: no
10 | * Han: 3 (concealed) / 2 (open)
11 | *
12 | * @implements HanCalculation.Yaku
13 | * @memberof HanCalculation
14 | */
15 | class JunchanYaku {
16 | /** @override */
17 | check ({ combinations, isOpen }) {
18 | let nbChii = 0
19 |
20 | for (const combination of combinations) {
21 | if (combination instanceof Sequence) nbChii++
22 |
23 | const nbTerminal = combination.tiles.reduce((agg, tile) => {
24 | if (tile instanceof NumberedTile && tile.isTerminal()) agg++
25 | return agg
26 | }, 0)
27 |
28 | if (nbTerminal === 0) return
29 | }
30 |
31 | if (nbChii > 0) return { key: 'junchan', hanValue: isOpen ? 2 : 3, yakumanValue: 0 }
32 | }
33 | }
34 |
35 | export default JunchanYaku
36 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/kokushi-musou-yaku.js:
--------------------------------------------------------------------------------
1 | import { Pair, Orphan } from './../../combination-classes'
2 | import { HonorTile } from './../../tile-classes'
3 |
4 | /**
5 | * Kokushi Musou (Thirteen Orphans)
6 | *
7 | * A hand with one of each dragon, wind and 1 and 9 of each suit, plus an other tile to make a pair
8 | * Worth two yakuman if the hand was waiting on 13 different tiles (Kokushi Musou 13 men machi)
9 | *
10 | * Must be concealed: yes
11 | * Yakuman: 1 / 2 (Waiting on 13 different tiles)
12 | *
13 | * @implements HanCalculation.Yaku
14 | * @memberof HanCalculation
15 | */
16 | class KokushiMusouYaku {
17 | constructor (options) {
18 | this.allowDoubleYakuman = options?.allowDoubleYakuman ?? false
19 | }
20 |
21 | /** @override */
22 | check ({ concealedCombinations, winningCombination }) {
23 | const nbOfPair = concealedCombinations.filter(x => x instanceof Pair).length
24 | if (nbOfPair === 0) return
25 |
26 | const nbOfOrphan = concealedCombinations.filter(x => x instanceof Orphan).length
27 | if (nbOfOrphan !== 12) return
28 |
29 | const nbOfValidDistinctTile = concealedCombinations.reduce((tiles, combination) => {
30 | combination.tiles.forEach(tile => tiles.push(tile))
31 | return tiles
32 | }, [])
33 | .filter(tile => tile instanceof HonorTile || tile.isTerminal())
34 | .map(tile => `${tile.suit}.${tile.value}`)
35 | .filter((key, index, keys) => keys.indexOf(key) === index)
36 | .length
37 |
38 | if (nbOfValidDistinctTile !== 13) return
39 |
40 | const yakumanValue = winningCombination instanceof Pair && this.allowDoubleYakuman ? 2 : 1
41 |
42 | return { key: 'kokushi musou', hanValue: 0, yakumanValue }
43 | }
44 | }
45 |
46 | export default KokushiMusouYaku
47 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/menzen-tsumo-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Menzen Tsumo (Fully Concealed Hand) yaku pattern
3 | *
4 | * Going out on self-draw with a concealed hand.
5 | *
6 | * Must be concealed: yes
7 | * Han: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class MenzenTsumoYaku {
13 | /** @override */
14 | check ({ winningType, isOpen }) {
15 | if (winningType === 'tsumo' && !isOpen) {
16 | return { key: 'menzen tsumo', hanValue: 1, yakumanValue: 0 }
17 | }
18 | }
19 | }
20 |
21 | export default MenzenTsumoYaku
22 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/open-riichi-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Open Riichi optional yaku pattern
3 | *
4 | * The principle works exactly in the same way as the original riichi. However, there is an added bonus of 1-han attached, which is earned by revealing either the hand or the tile waits.
5 | *
6 | * Must be concealed: yes
7 | * Han: 1 (or yakuman if ron in some ruleset)
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class OpenRiichiYaku {
13 | constructor (options) {
14 | this.ronAsYakuman = options?.ronAsYakuman ?? false
15 | }
16 |
17 | /** @override */
18 | check ({ yakus, winningType }) {
19 | if (yakus.includes('open riichi')) {
20 | if (winningType === 'ron' && this.ronAsYakuman) return { key: 'open riichi', hanValue: 0, yakumanValue: 1 }
21 | return { key: 'open riichi', hanValue: 1, yakumanValue: 0 }
22 | }
23 | }
24 | }
25 |
26 | export default OpenRiichiYaku
27 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/pinfu-yaku.js:
--------------------------------------------------------------------------------
1 | import { Pair, Sequence } from './../../combination-classes'
2 | import { DragonTile, WindTile } from './../../tile-classes'
3 | import isTankiWait from './../../waits/is-tanki-wait'
4 | import isKanchanWait from './../../waits/is-kanchan-wait'
5 | import isPenchanWait from './../../waits/is-penchan-wait'
6 |
7 | /**
8 | * Pinfu (All sequence / No points) yaku pattern
9 | *
10 | * A hand with no fu except the one for winning
11 | * Just sequence, no pair point (dragon or seat/prevalent wind) and a two-sided wait (only wait that give no fu)
12 | *
13 | * Must be concealed: yes
14 | * Han: 1
15 | *
16 | * @implements HanCalculation.Yaku
17 | * @memberof HanCalculation
18 | */
19 | class PinfuYaku {
20 | /** @override */
21 | check (hand) {
22 | if (
23 | hand.isOpen ||
24 | isTankiWait(hand) ||
25 | isKanchanWait(hand) ||
26 | isPenchanWait(hand)
27 | ) return
28 |
29 | const pairs = hand.concealedCombinations.filter(x => x instanceof Pair)
30 | const chiis = hand.concealedCombinations.filter(x => x instanceof Sequence)
31 |
32 | if (pairs.length !== 1 || chiis.length !== 4) return
33 |
34 | const pairTile = pairs[0].tiles[0]
35 |
36 | if (
37 | pairTile instanceof DragonTile ||
38 | (pairTile instanceof WindTile && (pairTile.value === hand.roundWind || pairTile.value === hand.seatWind))
39 | ) return
40 |
41 | return { key: 'pinfu', hanValue: 1, yakumanValue: 0 }
42 | }
43 | }
44 |
45 | export default PinfuYaku
46 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/renhou-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Renhou (Hand of man) yaku pattern
3 | *
4 | * A hand won by a non-dealer with a discard tile on his first round without open-meld
5 | *
6 | * Must be concealed: yes
7 | * Han: usually 5 but some rule make it a yakuman
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class RenhouYaku {
13 | /**
14 | * @param {Object} options - Configuration options for the yaku
15 | * @param {number} [options.hanValue=5] - Han value return if the yaku is valid
16 | * @param {number} [options.yakumanValue=0] - Yakuman value return if the yaku is valid
17 | */
18 | constructor (options) {
19 | this.hanValue = options?.hanValue ?? 5
20 | this.yakumanValue = options?.yakumanValue ?? 0
21 | }
22 |
23 | /** @override */
24 | check ({ yakus }) {
25 | if (yakus.includes('renhou')) return { key: 'renhou', hanValue: this.hanValue, yakumanValue: this.yakumanValue }
26 | }
27 | }
28 |
29 | export default RenhouYaku
30 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/riichi-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Riichi yaku pattern
3 | *
4 | * Waiting hand with declaration and 1000 point buy in.
5 | *
6 | * Must be concealed: yes
7 | * Han: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class RiichiYaku {
13 | /** @override */
14 | check ({ yakus }) {
15 | if (yakus.includes('riichi')) return { key: 'riichi', hanValue: 1, yakumanValue: 0 }
16 | }
17 | }
18 |
19 | export default RiichiYaku
20 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/rinshan-kaihou-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Rinshan Kaihou (After Kan) yaku pattern
3 | *
4 | * Winning after drawing a replacement tile.
7 | * Han: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class RinshanKaihouYaku {
13 | /** @override */
14 | check ({ yakus }) {
15 | if (yakus.includes('rinshan kaihou')) return { key: 'rinshan kaihou', hanValue: 1, yakumanValue: 0 }
16 | }
17 | }
18 |
19 | export default RinshanKaihouYaku
20 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/ryanpeikou-yaku.js:
--------------------------------------------------------------------------------
1 | import { Sequence } from './../../combination-classes'
2 |
3 | /**
4 | * Ryanpeikou (twice pure double chiis) yaku pattern
5 | *
6 | * Two pair of chiis, where each pair consists of two identical chiis.
7 | *
8 | * Must be concealed: yes (some rules say no)
9 | * Han: 3 (2 if open and the rule accept it)
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class RyanpeikouYaku {
15 | /**
16 | * @param {Object} options - Configuration options for the yaku
17 | * @param {boolean} [options.allowOpen=true] - Flag indicating if the yaku is valid with an open hand (kuitan)
18 | */
19 | constructor (options) {
20 | this.allowOpen = options?.allowOpen ?? false
21 | }
22 |
23 | /** @override */
24 | check ({ combinations, isOpen }) {
25 | if (!this.allowOpen && isOpen) return
26 |
27 | const groupsOfIdenticalSequence = combinations.filter(combination => combination instanceof Sequence).reduce((agg, combination) => {
28 | const key = combination.tiles[0].number + combination.tiles[0].suit
29 | const index = agg.findIndex(x => x.key === key)
30 | if (index === -1) {
31 | agg.push({ key, items: [combination] })
32 | } else {
33 | agg[index].items.push(combination)
34 | }
35 | return agg
36 | }, [])
37 |
38 | if (groupsOfIdenticalSequence.filter(x => x.items.length > 1).length === 2) {
39 | return { key: 'ryanpeikou', hanValue: isOpen ? 2 : 3, yakumanValue: 0 }
40 | }
41 | }
42 | }
43 |
44 | export default RyanpeikouYaku
45 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/ryuuiisou-yaku.js:
--------------------------------------------------------------------------------
1 | import { DragonTile, BambooTile } from './../../tile-classes'
2 |
3 | /**
4 | * Ryuu Iisou (All Green)
5 | *
6 | * A hand with only green tiles (2, 3, 4, 6, 8 of bamboo and green dragons)
7 | *
8 | * Must be concealed: no
9 | * Yakuman: 1
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class RyuuiisouYaku {
15 | /** @override */
16 | check ({ combinations }) {
17 | for (const combination of combinations) {
18 | for (const tile of combination.tiles) {
19 | if (!(
20 | (tile instanceof DragonTile && tile.color === 'green') ||
21 | (tile instanceof BambooTile && [2, 3, 4, 6, 8].includes(tile.number))
22 | )) {
23 | return
24 | }
25 | }
26 | }
27 |
28 | return { key: 'ryuuiisou', hanValue: 0, yakumanValue: 1 }
29 | }
30 | }
31 |
32 | export default RyuuiisouYaku
33 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/sanankou-yaku.js:
--------------------------------------------------------------------------------
1 | import { Triplet, Quad } from './../../combination-classes'
2 |
3 | /**
4 | * San Ankou (3 concealed pons) yaku pattern
5 | *
6 | * A hand with three concealed pons or kans.
7 | *
8 | * Must be concealed: no
9 | * Han: 2
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class SanankouYaku {
15 | /** @override */
16 | check ({ concealedCombinations, winningCombinationIndex, winningType }) {
17 | const nbAnkouOrAnkan = concealedCombinations.reduce((agg, combination, combinationIndex) => {
18 | if (
19 | (combination instanceof Triplet || combination instanceof Quad) &&
20 | (combinationIndex !== winningCombinationIndex || winningType === 'tsumo')
21 | ) {
22 | agg++
23 | }
24 |
25 | return agg
26 | }, 0)
27 |
28 | if (nbAnkouOrAnkan >= 3) return { key: 'sanankou', hanValue: 2, yakumanValue: 0 }
29 | }
30 | }
31 |
32 | export default SanankouYaku
33 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/sankantsu-yaku.js:
--------------------------------------------------------------------------------
1 | import { Quad } from './../../combination-classes'
2 |
3 | /**
4 | * San Kantsu (3 kans) yaku pattern
5 | *
6 | * A hand with three kans.
7 | *
8 | * Must be concealed: no
9 | * Han: 2
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class SankantsuYaku {
15 | /** @override */
16 | check ({ combinations }) {
17 | const nbKan = combinations.filter(x => x instanceof Quad).length
18 | if (nbKan === 3) return { key: 'sankantsu', hanValue: 2, yakumanValue: 0 }
19 | }
20 | }
21 |
22 | export default SankantsuYaku
23 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/sanshoku-doujun-yaku.js:
--------------------------------------------------------------------------------
1 | import { Sequence } from './../../combination-classes'
2 |
3 | /**
4 | * San Shoku Doujun (mixed triple sequence) yaku pattern
5 | *
6 | * Three chiis of the same value, with one in each suit
7 | *
8 | * Must be concealed: no
9 | * Han: 2 (concealed) / 1 (open)
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class SanshokuDoujunYaku {
15 | /** @override */
16 | check ({ combinations, isOpen }) {
17 | const chiiCount = {}
18 |
19 | for (const combination of combinations) {
20 | if (combination instanceof Sequence) {
21 | const tile = combination.tiles[0]
22 |
23 | if (chiiCount[tile.number] == null) {
24 | chiiCount[tile.number] = { dot: 0, bamboo: 0, character: 0 }
25 | }
26 |
27 | const count = chiiCount[tile.number]
28 |
29 | count[tile.suit]++
30 |
31 | if (count.dot && count.bamboo && count.character) {
32 | return { key: 'sanshoku doujun', hanValue: isOpen ? 1 : 2, yakumanValue: 0 }
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
39 | export default SanshokuDoujunYaku
40 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/sanshoku-doukou-yaku.js:
--------------------------------------------------------------------------------
1 | import { Triplet, Quad } from './../../combination-classes'
2 | import { NumberedTile } from './../../tile-classes'
3 |
4 | /**
5 | * San Shoku Doukou (triple triplet) yaku pattern
6 | *
7 | * One triplet or quad in each of the three suits, all having the same number.
8 | *
9 | * Must be concealed: no
10 | * Han: 2
11 | *
12 | * @implements HanCalculation.Yaku
13 | * @memberof HanCalculation
14 | */
15 | class SanshokuDokouYaku {
16 | /** @override */
17 | check (hand) {
18 | const storedPons = {}
19 |
20 | for (const combination of hand.combinations) {
21 | if (combination instanceof Triplet || combination instanceof Quad) {
22 | const tile = combination.tiles[0]
23 |
24 | if (tile instanceof NumberedTile) {
25 | if (storedPons[tile.number] == null) storedPons[tile.number] = 0
26 |
27 | storedPons[tile.number]++
28 |
29 | if (storedPons[tile.number] === 3) return { key: 'sanshoku doukou', hanValue: 2, yakumanValue: 0 }
30 | }
31 | }
32 | }
33 | }
34 | }
35 |
36 | export default SanshokuDokouYaku
37 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/shousangen-yaku.js:
--------------------------------------------------------------------------------
1 | import { Pair, Triplet, Quad } from './../../combination-classes'
2 | import { DragonTile } from './../../tile-classes'
3 |
4 | /**
5 | * Shou Sangen (little three dragons) yaku pattern
6 | *
7 | * Two pons/kans of dragons plus one pair of dragons.
8 | *
9 | * Must be concealed: no
10 | * Han: 2
11 | *
12 | * @implements HanCalculation.Yaku
13 | * @memberof HanCalculation
14 | */
15 | class ShousangenYaku {
16 | /** @override */
17 | check ({ combinations }) {
18 | const dragonCombinationCount = combinations.reduce((agg, combination) => {
19 | const tile = combination.tiles[0]
20 |
21 | if (tile instanceof DragonTile) {
22 | if (combination instanceof Pair) {
23 | agg.pair++
24 | } else if (combination instanceof Triplet || combination instanceof Quad) {
25 | agg.ponOrKan++
26 | }
27 | }
28 |
29 | return agg
30 | }, { pair: 0, ponOrKan: 0 })
31 |
32 | if (dragonCombinationCount.pair === 1 && dragonCombinationCount.ponOrKan === 2) {
33 | return { key: 'shousangen', hanValue: 2, yakumanValue: 0 }
34 | }
35 | }
36 | }
37 |
38 | export default ShousangenYaku
39 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/shousuushii-yaku.js:
--------------------------------------------------------------------------------
1 | import { Triplet, Quad, Pair } from './../../combination-classes'
2 | import { WindTile } from './../../tile-classes'
3 |
4 | /**
5 | * Shou Suushii (Little Four Winds)
6 | *
7 | * A hand with three triplet/quad of winds and a pair of the fourth wind
8 | *
9 | * Must be concealed: no
10 | * Yakuman: 1
11 | *
12 | * @implements HanCalculation.Yaku
13 | * @memberof HanCalculation
14 | */
15 | class ShousuushiiYaku {
16 | /** @override */
17 | check ({ combinations }) {
18 | const nbOfWindPonOrKan = combinations.filter(x => (x instanceof Triplet || x instanceof Quad) && x.tiles[0] instanceof WindTile).length
19 | const nbOfWindPair = combinations.filter(x => x instanceof Pair && x.tiles[0] instanceof WindTile).length
20 |
21 | if (nbOfWindPonOrKan === 3 && nbOfWindPair === 1) {
22 | return { key: 'shousuushii', hanValue: 0, yakumanValue: 1 }
23 | }
24 | }
25 | }
26 |
27 | export default ShousuushiiYaku
28 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/suuankou-yaku.js:
--------------------------------------------------------------------------------
1 | import { Triplet, Quad } from './../../combination-classes'
2 | import isTankiWait from './../../waits/is-tanki-wait'
3 |
4 | /**
5 | * Suu Ankou (Four concealed pons)
6 | *
7 | * A hand that has four closed pons/kans
8 | * In the case of a single-tile wait for the pair, the tile can either be self-drawn or won from another player's discard, and it is worth two yakuman
9 | *
10 | * Must be concealed: yes
11 | * Yakuman: 1 (not single wait) / 2 (single wait)
12 | *
13 | * @implements HanCalculation.Yaku
14 | * @memberof HanCalculation
15 | */
16 | class SuuankouYaku {
17 | constructor (options) {
18 | this.allowDoubleYakuman = options?.allowDoubleYakuman ?? false
19 | }
20 |
21 | /** @override */
22 | check (hand) {
23 | const nbOfConcealedPonOrKan = hand.concealedCombinations.filter((x, i) =>
24 | (x instanceof Triplet || x instanceof Quad) &&
25 | (i !== hand.winningCombinationIndex || hand.winningType === 'tsumo')
26 | ).length
27 |
28 | if (nbOfConcealedPonOrKan === 4) {
29 | return { key: 'suuankou', hanValue: 0, yakumanValue: isTankiWait(hand) && this.allowDoubleYakuman ? 2 : 1 }
30 | }
31 | }
32 | }
33 |
34 | export default SuuankouYaku
35 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/suukantsu-yaku.js:
--------------------------------------------------------------------------------
1 | import { Quad } from './../../combination-classes'
2 |
3 | /**
4 | * Suu Kantsu (Four kans)
5 | *
6 | * A hand with four kans, which can be open or concealed
7 | *
8 | * Must be concealed: no
9 | * Yakuman: 1
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class SuukantsuYaku {
15 | /** @override */
16 | check ({ combinations }) {
17 | const nbOfKan = combinations.filter(x => x instanceof Quad).length
18 | if (nbOfKan === 4) return { key: 'suukantsu', hanValue: 0, yakumanValue: 1 }
19 | }
20 | }
21 |
22 | export default SuukantsuYaku
23 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/tanyao-yaku.js:
--------------------------------------------------------------------------------
1 | import { HonorTile } from './../../tile-classes'
2 |
3 | /**
4 | * Tanyao (all simples) yaku pattern
5 | *
6 | * A hand consisting only of suit tiles 2-8 (without terminal or honor tiles)
7 | *
8 | * Must be concealed : no (some rules say yes)
9 | * Han : 1
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class TanyaoYaku {
15 | /**
16 | * @param {Object} options - Configuration options for the yaku
17 | * @param {boolean} [options.allowOpen=true] - Flag indicating if the yaku is valid with an open hand (kuitan)
18 | */
19 | constructor (options) {
20 | this.allowOpen = options?.allowOpen ?? true
21 | }
22 |
23 | /** @override */
24 | check ({ combinations, isOpen }) {
25 | if (!this.allowOpen && isOpen) return
26 |
27 | for (const combination of combinations) {
28 | for (const tile of combination.tiles) {
29 | if (tile instanceof HonorTile || tile.isTerminal()) return
30 | }
31 | }
32 |
33 | return { key: 'tanyao', hanValue: 1, yakumanValue: 0 }
34 | }
35 | }
36 |
37 | export default TanyaoYaku
38 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/tenhou-yaku.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenhou (Heavenly Hand)
3 | *
4 | * A hand won by the dealer on his first draw
5 | *
6 | * Must be concealed: yes
7 | * Yakuman: 1
8 | *
9 | * @implements HanCalculation.Yaku
10 | * @memberof HanCalculation
11 | */
12 | class TenhouYaku {
13 | /** @override */
14 | check ({ yakus }) {
15 | if (yakus.includes('tenhou')) return { key: 'tenhou', hanValue: 0, yakumanValue: 1 }
16 | }
17 | }
18 |
19 | export default TenhouYaku
20 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/toitoi-yaku.js:
--------------------------------------------------------------------------------
1 | import { Triplet, Quad } from './../../combination-classes'
2 |
3 | /**
4 | * Toitoi (all pons) yaku pattern
5 |
6 | * A hand with four pons/kans and one pair.
7 | *
8 | * Must be concealed: no
9 | * Han: 2
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class ToitoiYaku {
15 | /** @override */
16 | check ({ combinations }) {
17 | const nbPonOrKan = combinations.reduce((agg, combination) => {
18 | if (combination instanceof Triplet || combination instanceof Quad) agg++
19 | return agg
20 | }, 0)
21 |
22 | if (nbPonOrKan === 4) return { key: 'toitoi', hanValue: 2, yakumanValue: 0 }
23 | }
24 | }
25 |
26 | export default ToitoiYaku
27 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/tsuuiisou-yaku.js:
--------------------------------------------------------------------------------
1 | import { HonorTile } from './../../tile-classes'
2 |
3 | /**
4 | * Tsuu Iisou (All Honors)
5 | *
6 | * A hand with only honor tiles (dragons and winds)
7 | *
8 | * Must be concealed: no
9 | * Yakuman: 1
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class TsuuiisouYaku {
15 | /** @override */
16 | check ({ combinations }) {
17 | for (const combination of combinations) {
18 | for (const tile of combination.tiles) {
19 | if (!(tile instanceof HonorTile)) return
20 | }
21 | }
22 |
23 | return { key: 'tsuuiisou', hanValue: 0, yakumanValue: 1 }
24 | }
25 | }
26 |
27 | export default TsuuiisouYaku
28 |
--------------------------------------------------------------------------------
/src/core/han-calculation/yakus/yakuhai-yaku.js:
--------------------------------------------------------------------------------
1 | import { Triplet, Quad } from './../../combination-classes'
2 |
3 | /**
4 | * Yakuhai yaku pattern
5 | *
6 | * A triplet or quad of dragon, round wind or seat wind
7 | *
8 | * Must be concealed: no
9 | * Han: 1
10 | *
11 | * @implements HanCalculation.Yaku
12 | * @memberof HanCalculation
13 | */
14 | class YakuhaiYaku {
15 | /** @override */
16 | check ({ combinations, roundWind, seatWind }) {
17 | const yaku = combinations.reduce((agg, combination) => {
18 | if (combination instanceof Triplet || combination instanceof Quad) {
19 | const tile = combination.tiles[0]
20 |
21 | if (tile.value === 'red') agg.push({ key: 'chun', hanValue: 1, yakumanValue: 0 })
22 | else if (tile.value === 'green') agg.push({ key: 'hatsu', hanValue: 1, yakumanValue: 0 })
23 | else if (tile.value === 'white') agg.push({ key: 'haku', hanValue: 1, yakumanValue: 0 })
24 | else if (tile.value === roundWind || tile.value === seatWind) {
25 | agg.push({ key: mapWindKey(tile.value), hanValue: roundWind === seatWind ? 2 : 1, yakumanValue: 0 })
26 | }
27 | }
28 |
29 | return agg
30 | }, [])
31 |
32 | if (yaku.length > 0) return yaku
33 | }
34 | }
35 |
36 | function mapWindKey (wind) {
37 | if (wind === 'east') return 'ton'
38 | else if (wind === 'south') return 'nan'
39 | else if (wind === 'west') return 'xia'
40 | else if (wind === 'north') return 'pei'
41 | }
42 |
43 | export default YakuhaiYaku
44 |
--------------------------------------------------------------------------------
/src/core/hand.js:
--------------------------------------------------------------------------------
1 | class Hand {
2 | constructor (options) {
3 | this.concealedCombinations = options?.concealedCombinations ?? []
4 | this.openCombinations = options?.openCombinations ?? []
5 |
6 | this.winningType = options?.winningType ?? 'tsumo'
7 | this.seatWind = options?.seatWind ?? 'east'
8 | this.roundWind = options?.roundWind ?? 'east'
9 |
10 | this.winningCombinationIndex = options?.winningCombinationIndex ?? null
11 | this.winningTileIndex = options?.winningTileIndex ?? null
12 |
13 | this.nbDora = options?.nbDora ?? 0
14 |
15 | this.yakus = options?.yakus ?? []
16 | }
17 |
18 | get isOpen () {
19 | return this.openCombinations.length > 0
20 | }
21 |
22 | get combinations () {
23 | return this.concealedCombinations.concat(this.openCombinations)
24 | }
25 |
26 | get winningCombination () {
27 | if (this.winningCombinationIndex != null) {
28 | return this.concealedCombinations[this.winningCombinationIndex] ?? null
29 | }
30 | return null
31 | }
32 |
33 | get winningTile () {
34 | if (this.winningCombination != null && this.winningTileIndex != null) {
35 | return this.winningCombination.tiles[this.winningTileIndex] ?? null
36 | }
37 | return null
38 | }
39 | }
40 |
41 | export default Hand
42 |
--------------------------------------------------------------------------------
/src/core/point-calculation/point-calculator.js:
--------------------------------------------------------------------------------
1 | class PointCalculator {
2 | constructor (options) {
3 | this.kazoeYakumanAsSanbaiman = options?.kazoeYakumanAsSanbaiman ?? true
4 | this.kiriageMangan = options?.kiriageMangan ?? false
5 | }
6 |
7 | calculate (hand, fu, han, yakuman) {
8 | if (yakuman > 0) return getPointForYakuman(hand, yakuman)
9 | if (han >= 13 && !this.kazoeYakumanAsSanbaiman) return getPointForYakuman(hand, 1)
10 | if (han >= 13 && this.kazoeYakumanAsSanbaiman) return getPointForSanbaiman(hand)
11 | if (han >= 11) return getPointForSanbaiman(hand)
12 | if (han >= 8) return getPointForBaiman(hand)
13 | if (han >= 6) return getPointForHaneman(hand)
14 | if (han === 5 || (this.kiriageMangan && canBeRoundUpToMangan(fu, han))) return getPointForMangan(hand)
15 | if ((han === 3 && fu > 60) || (han === 4 && fu > 30)) return getPointForMangan(hand)
16 | return getPointFromManualCalculation(hand, fu, han)
17 | }
18 | }
19 |
20 | function canBeRoundUpToMangan (fu, han) {
21 | return (han === 4 && fu === 30) || (han === 3 && fu === 60)
22 | }
23 |
24 | function getPointForYakuman (hand, yakuman) {
25 | return multiplyBy(yakuman, multiplyBy(4, (getPointForMangan(hand))))
26 | }
27 |
28 | function getPointForSanbaiman (hand) {
29 | return multiplyBy(3, getPointForMangan(hand))
30 | }
31 |
32 | function getPointForBaiman (hand) {
33 | return multiplyBy(2, getPointForMangan(hand))
34 | }
35 |
36 | function getPointForHaneman (hand) {
37 | return multiplyBy(1.5, getPointForMangan(hand))
38 | }
39 |
40 | function getPointForMangan (hand) {
41 | if (hand.seatWind === 'east' && hand.winningType === 'tsumo') return { nonDealer: 4000 }
42 | if (hand.seatWind === 'east' && hand.winningType === 'ron') return { discard: 12000 }
43 | if (hand.seatWind !== 'east' && hand.winningType === 'tsumo') return { dealer: 4000, nonDealer: 2000 }
44 | if (hand.seatWind !== 'east' && hand.winningType === 'ron') return { discard: 8000 }
45 | }
46 |
47 | function getPointFromManualCalculation (hand, fu, han) {
48 | const basicPoints = fu * Math.pow(2, 2 + han)
49 |
50 | if (hand.seatWind === 'east' && hand.winningType === 'tsumo') {
51 | return { nonDealer: roundUpToTheHundreds(basicPoints * 2) }
52 | }
53 |
54 | if (hand.seatWind === 'east' && hand.winningType === 'ron') {
55 | return { discard: roundUpToTheHundreds(basicPoints * 6) }
56 | }
57 |
58 | if (hand.seatWind !== 'east' && hand.winningType === 'tsumo') {
59 | return { dealer: roundUpToTheHundreds(basicPoints * 2), nonDealer: roundUpToTheHundreds(basicPoints) }
60 | }
61 |
62 | if (hand.seatWind !== 'east' && hand.winningType === 'ron') {
63 | return { discard: roundUpToTheHundreds(basicPoints * 4) }
64 | }
65 | }
66 |
67 | function roundUpToTheHundreds (value) {
68 | return Math.ceil(value / 100) * 100
69 | }
70 |
71 | function multiplyBy (factor, source) {
72 | if (source.discard != null) {
73 | return { discard: source.discard * factor }
74 | }
75 |
76 | if (source.nonDealer != null && source.dealer == null) {
77 | return { nonDealer: source.nonDealer * factor }
78 | }
79 |
80 | return { dealer: source.dealer * factor, nonDealer: source.nonDealer * factor }
81 | }
82 |
83 | export default PointCalculator
84 |
--------------------------------------------------------------------------------
/src/core/tile-classes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Tile factory class
3 | * Factory design pattern to help create tile classes
4 | *
5 | * ex. : TileFactory.create('dragon', 'red')
6 | */
7 | export const TileFactory = {
8 | create (suit, value) {
9 | switch (suit) {
10 | case 'dragon':
11 | return new DragonTile(value)
12 | case 'wind':
13 | return new WindTile(value)
14 | case 'bamboo':
15 | return new BambooTile(value)
16 | case 'dot':
17 | return new DotTile(value)
18 | case 'character':
19 | return new CharacterTile(value)
20 | default:
21 | throw new Error(`Tile Factory Error : "${suit}" is not a supported suit`)
22 | }
23 | }
24 | }
25 |
26 | /**
27 | * Tile base class
28 | * All tile inherit from this class
29 | */
30 | export class Tile {
31 | constructor (suit, value) {
32 | this.suit = suit
33 | this.value = value
34 | }
35 | }
36 |
37 | /**
38 | * Honor tile base class
39 | * Wind and Dragon tile inherit from this class
40 | */
41 | export class HonorTile extends Tile {}
42 |
43 | /**
44 | * Numbered tile base class
45 | * Dot, Character and Bamboo tile inherit from this class
46 | */
47 | export class NumberedTile extends Tile {
48 | constructor (suit, value) {
49 | super(suit, value)
50 | this.number = value
51 | }
52 |
53 | isTerminal () {
54 | return this.number === 1 || this.number === 9
55 | }
56 | }
57 |
58 | /**
59 | * Wind tile class
60 | * East, South, West and North tile
61 | */
62 | export class WindTile extends HonorTile {
63 | constructor (direction) {
64 | super('wind', direction) // east, south, west, north
65 | this.direction = direction
66 | }
67 | }
68 |
69 | /**
70 | * Dragon tile class
71 | * Red, Green and White dragon tile
72 | */
73 | export class DragonTile extends HonorTile {
74 | constructor (color) {
75 | super('dragon', color) // red, green, white
76 | this.color = color
77 | }
78 | }
79 |
80 | /**
81 | * Dot tile class
82 | */
83 | export class DotTile extends NumberedTile {
84 | constructor (number) {
85 | super('dot', number)
86 | }
87 | }
88 |
89 | /**
90 | * Character tile class
91 | */
92 | export class CharacterTile extends NumberedTile {
93 | constructor (number) {
94 | super('character', number)
95 | }
96 | }
97 |
98 | /**
99 | * Bamboo tile class
100 | */
101 | export class BambooTile extends NumberedTile {
102 | constructor (number) {
103 | super('bamboo', number)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/core/waits/is-kanchan-wait.js:
--------------------------------------------------------------------------------
1 | import { Sequence } from './../combination-classes'
2 |
3 | function isKanchanWait ({ concealedCombinations, winningCombinationIndex, winningTileIndex }) {
4 | const winningCombination = concealedCombinations[winningCombinationIndex]
5 | return winningCombination != null && winningCombination instanceof Sequence && winningTileIndex === 1
6 | }
7 |
8 | export default isKanchanWait
9 |
--------------------------------------------------------------------------------
/src/core/waits/is-penchan-wait.js:
--------------------------------------------------------------------------------
1 | import { Sequence } from './../combination-classes'
2 |
3 | function isPenchanWait ({ concealedCombinations, winningCombinationIndex, winningTileIndex }) {
4 | const winningCombination = concealedCombinations[winningCombinationIndex]
5 |
6 | if (winningCombination != null && winningCombination instanceof Sequence) {
7 | const winningTile = winningCombination.tiles[winningTileIndex]
8 |
9 | if (
10 | winningTile != null &&
11 | (
12 | (winningTileIndex === 0 && winningTile.value === 7) ||
13 | (winningTileIndex === 2 && winningTile.value === 3)
14 | )
15 | ) {
16 | return true
17 | }
18 | }
19 |
20 | return false
21 | }
22 |
23 | export default isPenchanWait
24 |
--------------------------------------------------------------------------------
/src/core/waits/is-tanki-wait.js:
--------------------------------------------------------------------------------
1 | import { Pair, Orphan } from './../combination-classes'
2 |
3 | function isTankiWait ({ concealedCombinations, winningCombinationIndex }) {
4 | const nbOrphans = concealedCombinations.filter(x => x instanceof Orphan).length
5 | const winningCombination = concealedCombinations[winningCombinationIndex]
6 | return winningCombination != null && winningCombination instanceof Pair && nbOrphans === 0
7 | }
8 |
9 | export default isTankiWait
10 |
--------------------------------------------------------------------------------
/src/core/wrc/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @namespace Wrc
3 | */
4 |
--------------------------------------------------------------------------------
/src/core/wrc/wrc-fu-calculator.js:
--------------------------------------------------------------------------------
1 | import FuCalculator from './../fu-calculation/fu-calculator'
2 |
3 | import WinRule from './../fu-calculation/rules/winning-fu-rule'
4 | import WaitRule from './../fu-calculation/rules/wait-fu-rule'
5 | import TsumoRule from './../fu-calculation/rules/tsumo-fu-rule'
6 | import PairRule from './../fu-calculation/rules/pair-fu-rule'
7 | import OpenPinfuRule from './../fu-calculation/rules/open-pinfu-fu-rule'
8 | import CombinationsRule from './../fu-calculation/rules/combinations-fu-rule'
9 | import ClosedRonRule from './../fu-calculation/rules/closed-ron-fu-rule'
10 | import ChiitoitsuRule from './../fu-calculation/rules/chiitoitsu-fu-rule'
11 |
12 | import ChiitoitsuYaku from './../han-calculation/yakus/chiitoitsu-yaku'
13 | import PinfuYaku from './../han-calculation/yakus/pinfu-yaku'
14 |
15 | /**
16 | * Fu calculator implementation using WRC ruleset.
17 | *
18 | * @memberof Wrc
19 | */
20 | class WrcFuCalculator extends FuCalculator {
21 | constructor () {
22 | super([
23 | // put the chiitoitsu rule at the top because it's a fixed amount of fu
24 | new ChiitoitsuRule({ chiitoitsuYakuPattern: new ChiitoitsuYaku(), fuValue: 25 }),
25 | new WinRule(),
26 | new CombinationsRule(),
27 | new PairRule({ stackable: false }),
28 | new WaitRule(),
29 | new OpenPinfuRule(),
30 | new ClosedRonRule(),
31 | new TsumoRule({ excludedYakuPatterns: [new PinfuYaku()] })
32 | ])
33 | }
34 | }
35 |
36 | export default WrcFuCalculator
37 |
--------------------------------------------------------------------------------
/src/core/wrc/wrc-point-calculator.js:
--------------------------------------------------------------------------------
1 | import PointCalculator from './../point-calculation/point-calculator'
2 |
3 | class WrcPointCalculator extends PointCalculator {
4 | constructor () {
5 | super({
6 | kazoeYakumanAsSanbaiman: true,
7 | kiriageMangan: true
8 | })
9 | }
10 | }
11 |
12 | export default WrcPointCalculator
13 |
--------------------------------------------------------------------------------
/src/event-bus.js:
--------------------------------------------------------------------------------
1 | import emitter from 'tiny-emitter/instance'
2 |
3 | export default {
4 | on: (...args) => emitter.on(...args),
5 | once: (...args) => emitter.once(...args),
6 | off: (...args) => emitter.off(...args),
7 | emit: (...args) => emitter.emit(...args)
8 | }
9 |
--------------------------------------------------------------------------------
/src/filters/format-number.js:
--------------------------------------------------------------------------------
1 | export default value => {
2 | if (value == null) return value
3 | return Intl.NumberFormat('en-US').format(value)
4 | }
5 |
--------------------------------------------------------------------------------
/src/filters/title-case.js:
--------------------------------------------------------------------------------
1 | const smallWords = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|v.?|vs.?|via)$/i
2 | const alphanumericPattern = /([A-Za-z0-9\u00C0-\u00FF])/
3 | const wordSeparators = /([ :–—-])/
4 |
5 | export default str => {
6 | if (str == null) return str
7 |
8 | return str.split(wordSeparators)
9 | .map((current, index, array) => {
10 | if (
11 | /* Check for small words */
12 | current.search(smallWords) > -1 &&
13 | /* Skip first and last word */
14 | index !== 0 &&
15 | index !== array.length - 1 &&
16 | /* Ignore title end and subtitle start */
17 | array[index - 3] !== ':' &&
18 | array[index + 1] !== ':' &&
19 | /* Ignore small words that start a hyphenated phrase */
20 | (array[index + 1] !== '-' ||
21 | (array[index - 1] === '-' && array[index + 1] === '-'))
22 | ) {
23 | return current.toLowerCase()
24 | }
25 |
26 | /* Ignore intentional capitalization */
27 | if (current.substr(1).search(/[A-Z]|\../) > -1) {
28 | return current
29 | }
30 |
31 | /* Ignore URLs */
32 | if (array[index + 1] === ':' && array[index + 2] !== '') {
33 | return current
34 | }
35 |
36 | /* Capitalize the first letter */
37 | return current.replace(alphanumericPattern, match => match.toUpperCase())
38 | })
39 | .join('')
40 | }
41 |
--------------------------------------------------------------------------------
/src/i18n.js:
--------------------------------------------------------------------------------
1 | import { getProperty } from 'dot-prop'
2 | import en from './locales/en.json'
3 | import jpRomanized from './locales/jp-romanized.json'
4 |
5 | const defaultLocale = 'en'
6 | const messages = {
7 | en,
8 | 'jp-romanized': jpRomanized
9 | }
10 |
11 | export const t = (key, locale) => getProperty(messages[locale ?? defaultLocale], key) ?? key
12 |
--------------------------------------------------------------------------------
/src/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "chankan": "robbing a kan",
3 | "chanta": "half outside hand",
4 | "chiihou": "blessing of earth",
5 | "chiitoitsu": "seven pairs",
6 | "chinitsu": "full flush",
7 | "chinroutou": "all terminals",
8 | "chun": "red dragon",
9 | "chuuren poutou": "nine gates",
10 | "daisangen": "big three dragons",
11 | "daisuushii": "four big winds",
12 | "double riichi": "double riichi",
13 | "dora": "dora",
14 | "haitei raoyue": "under the sea",
15 | "haku": "white dragon",
16 | "hatsu": "green dragon",
17 | "honitsu": "half flush",
18 | "honroutou": "all terminals and honours",
19 | "houtei raoyui": "under the river",
20 | "iipeikou": "pure double sequence",
21 | "ippatsu": "ippatsu",
22 | "ittsuu": "pure straight",
23 | "junchan": "fully outside hand",
24 | "kokushi musou": "thirteen orphans",
25 | "menzen tsumo": "fully concealed hand",
26 | "nan": "south",
27 | "pei": "north",
28 | "pinfu": "pinfu",
29 | "renhou": "blessing of man",
30 | "riichi": "riichi",
31 | "rinshan kaihou": "after a kan",
32 | "ryanpeikou": "twice pure double sequence",
33 | "ryuuiisou": "all green",
34 | "sanankou": "three concealed triplets",
35 | "sankantsu": "three quads",
36 | "sanshoku doujun": "mixed triple sequence",
37 | "sanshoku doukou": "triple triplets",
38 | "shousangen": "little three dragons",
39 | "shousuushii": "four little winds",
40 | "suuankou": "four concealed triplets",
41 | "suukantsu": "four quads",
42 | "tanyao": "all simples",
43 | "tenhou": "blessing of heaven",
44 | "toitoi": "all triplets",
45 | "ton": "east",
46 | "tsuuiisou": "all honours",
47 | "xia": "west",
48 |
49 | "fuRules": {
50 | "win": "Winning",
51 | "tsumo": "Tsumo",
52 | "closed ron": "concealed ron",
53 | "open pinfu": "open pinfu",
54 | "chiitoitsu": "chiitoitsu (seven pairs)",
55 | "pair": "pair",
56 | "wait": "wait",
57 | "minkou simple": "open triplet (simple)",
58 | "minkou non simple": "open triplet (terminal / honor)",
59 | "minkan simple": "open quad (simple)",
60 | "minkan non simple": "open quad (terminal / honor)",
61 | "ankou simple": "concealed triplet (simple)",
62 | "ankou non simple": "concealed triplet (terminal / honor)",
63 | "ankan simple": "concealed quad (simple)",
64 | "ankan non simple": "concealed quad (terminal / honor)"
65 | }
66 | }
--------------------------------------------------------------------------------
/src/locales/jp-romanized.json:
--------------------------------------------------------------------------------
1 | {
2 | "chankan": "chankan",
3 | "chanta": "chanta",
4 | "chiihou": "chiihou",
5 | "chiitoitsu": "chiitoitsu",
6 | "chinitsu": "chinitsu",
7 | "chinroutou": "chinroutou",
8 | "chun": "chun",
9 | "chuuren poutou": "chuuren poutou",
10 | "daisangen": "daisangen",
11 | "daisuushii": "daisuushii",
12 | "double riichi": "double riichi",
13 | "dora": "dora",
14 | "haitei raoyue": "haitei raoyue",
15 | "haku": "haku",
16 | "hatsu": "hatsu",
17 | "honitsu": "honitsu",
18 | "honroutou": "honroutou",
19 | "houtei raoyui": "houtei raoyui",
20 | "iipeikou": "iipeikou",
21 | "ippatsu": "ippatsu",
22 | "ittsuu": "ittsuu",
23 | "junchan": "junchan",
24 | "kokushi musou": "kokushi musou",
25 | "menzen tsumo": "menzen tsumo",
26 | "nan": "nan",
27 | "pei": "pei",
28 | "pinfu": "pinfu",
29 | "renhou": "renhou",
30 | "riichi": "riichi",
31 | "rinshan kaihou": "rinshan kaihou",
32 | "ryanpeikou": "ryanpeikou",
33 | "ryuuiisou": "ryuuiisou",
34 | "sanankou": "sanankou",
35 | "sankantsu": "sankantsu",
36 | "sanshoku doujun": "sanshoku doujun",
37 | "sanshoku doukou": "sanshoku doukou",
38 | "shousangen": "shousangen",
39 | "shousuushii": "shousuushii",
40 | "suuankou": "suuankou",
41 | "suukantsu": "suukantsu",
42 | "tanyao": "tanyao",
43 | "tenhou": "tenhou",
44 | "toitoi": "toitoi",
45 | "ton": "ton",
46 | "tsuuiisou": "tsuuiisou",
47 | "xia": "xia"
48 | }
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 | import './assets/style.css'
4 |
5 | createApp(App).mount('#App')
6 |
--------------------------------------------------------------------------------
/src/ruleset.js:
--------------------------------------------------------------------------------
1 | import * as WrcRuleset from './rulesets/wrc-ruleset'
2 | import * as EmaRuleset from './rulesets/ema-ruleset'
3 | import * as CustomRuleset from './rulesets/custom-ruleset'
4 |
5 | const localStorageKey = 'riichi-pointer-ruleset'
6 | const defaultPreset = WrcRuleset.key
7 | let instance = null
8 |
9 | function setup (key, options) {
10 | if (key === WrcRuleset.key) {
11 | instance = WrcRuleset.create(options)
12 | } else if (key === EmaRuleset.key) {
13 | instance = EmaRuleset.create(options)
14 | } else if (key === CustomRuleset.key) {
15 | instance = CustomRuleset.create(options)
16 | } else {
17 | throw Error(`ruleset key [${key}] not supported`)
18 | }
19 |
20 | if (key === defaultPreset) {
21 | localStorage.removeItem(localStorageKey)
22 | } else if (key === CustomRuleset.key) {
23 | // only need to save the options for the custom ruleset for now because they are readonly for the other presets
24 | localStorage.setItem(localStorageKey, JSON.stringify({ key, options: instance.options }))
25 | } else {
26 | localStorage.setItem(localStorageKey, JSON.stringify({ key }))
27 | }
28 | }
29 |
30 | function initialize () {
31 | // try to find a previous ruleset in the localStorage of the browser to re-apply on the application
32 | try {
33 | const localStorageItem = localStorage.getItem(localStorageKey)
34 |
35 | if (localStorageItem != null) {
36 | const parsedLocalStorageItem = JSON.parse(localStorageItem)
37 | setup(parsedLocalStorageItem.key, parsedLocalStorageItem.options)
38 | }
39 | } catch { }
40 |
41 | // put the default preset if no previous ruleset was found or if an error occured while trying to setup it
42 | if (instance == null) setup(defaultPreset)
43 | }
44 |
45 | initialize()
46 |
47 | export function getRuleset () {
48 | return instance
49 | }
50 |
51 | export function setRuleset (key, options) {
52 | setup(key, options)
53 | }
54 |
--------------------------------------------------------------------------------
/src/rulesets/ema-ruleset.js:
--------------------------------------------------------------------------------
1 | import EmaFuCalculator from '../core/ema/ema-fu-calculator'
2 | import EmaHanCalculator from '../core/ema/ema-han-calculator'
3 | import EmaPointCalculator from '../core/ema/ema-point-calculator'
4 | import fourPlayerGameTiles from './helpers/four-player-game-tiles'
5 |
6 | // options are there only to be able to display them as readonly options in the configuration modal
7 | // if you need to change the implementation, please refer to ./../core/ema/* classes
8 | const options = {
9 | kazoeYakumanAsSanbaiman: true,
10 | allowMultipleYakuman: false,
11 | allowDoubleYakuman: false,
12 | allowOpenTanyao: true,
13 | allowDoubleWindFu: true,
14 | renhouValue: 'mangan',
15 | kiriageMangan: false,
16 |
17 | // optional yaku
18 | allowOpenRiichi: false
19 | }
20 |
21 | export const key = 'ema'
22 |
23 | export function create () {
24 | return {
25 | key,
26 | options,
27 |
28 | fuCalculator: new EmaFuCalculator(),
29 | hanCalculator: new EmaHanCalculator(),
30 | pointCalculator: new EmaPointCalculator(),
31 |
32 | tiles: fourPlayerGameTiles
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/rulesets/helpers/four-player-game-tiles.js:
--------------------------------------------------------------------------------
1 | import { TileFactory } from '@/core/tile-classes'
2 |
3 | const tiles = [
4 | TileFactory.create('dragon', 'green'),
5 | TileFactory.create('dragon', 'red'),
6 | TileFactory.create('dragon', 'white'),
7 |
8 | TileFactory.create('wind', 'east'),
9 | TileFactory.create('wind', 'south'),
10 | TileFactory.create('wind', 'west'),
11 | TileFactory.create('wind', 'north'),
12 |
13 | TileFactory.create('bamboo', 1),
14 | TileFactory.create('bamboo', 2),
15 | TileFactory.create('bamboo', 3),
16 | TileFactory.create('bamboo', 4),
17 | TileFactory.create('bamboo', 5),
18 | TileFactory.create('bamboo', 6),
19 | TileFactory.create('bamboo', 7),
20 | TileFactory.create('bamboo', 8),
21 | TileFactory.create('bamboo', 9),
22 |
23 | TileFactory.create('character', 1),
24 | TileFactory.create('character', 2),
25 | TileFactory.create('character', 3),
26 | TileFactory.create('character', 4),
27 | TileFactory.create('character', 5),
28 | TileFactory.create('character', 6),
29 | TileFactory.create('character', 7),
30 | TileFactory.create('character', 8),
31 | TileFactory.create('character', 9),
32 |
33 | TileFactory.create('dot', 1),
34 | TileFactory.create('dot', 2),
35 | TileFactory.create('dot', 3),
36 | TileFactory.create('dot', 4),
37 | TileFactory.create('dot', 5),
38 | TileFactory.create('dot', 6),
39 | TileFactory.create('dot', 7),
40 | TileFactory.create('dot', 8),
41 | TileFactory.create('dot', 9)
42 | ]
43 |
44 | export default tiles
45 |
--------------------------------------------------------------------------------
/src/rulesets/wrc-ruleset.js:
--------------------------------------------------------------------------------
1 | import WrcFuCalculator from './../core/wrc/wrc-fu-calculator'
2 | import WrcHanCalculator from './../core/wrc/wrc-han-calculator'
3 | import WrcPointCalculator from './../core/wrc/wrc-point-calculator'
4 | import fourPlayerGameTiles from './helpers/four-player-game-tiles'
5 |
6 | // options are there only to be able to display them as readonly options in the configuration modal
7 | // if you need to change the implementation, please refer to ./../core/wrc/* classes
8 | const options = {
9 | kazoeYakumanAsSanbaiman: true,
10 | allowMultipleYakuman: true,
11 | allowDoubleYakuman: false,
12 | allowOpenTanyao: true,
13 | allowDoubleWindFu: false,
14 | renhouValue: 'mangan',
15 | kiriageMangan: true,
16 |
17 | // optional yaku
18 | allowOpenRiichi: false
19 | }
20 |
21 | export const key = 'wrc'
22 |
23 | export function create () {
24 | return {
25 | key,
26 | options,
27 |
28 | fuCalculator: new WrcFuCalculator(),
29 | hanCalculator: new WrcHanCalculator(),
30 | pointCalculator: new WrcPointCalculator(),
31 |
32 | tiles: fourPlayerGameTiles
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tests/fixtures/dealer-score-fixture.json:
--------------------------------------------------------------------------------
1 | [
2 | [1, 30, 1500, 500],
3 | [1, 40, 2000, 700],
4 | [1, 50, 2400, 800],
5 | [1, 60, 2900, 1000],
6 | [1, 70, 3400, 1200],
7 | [1, 80, 3900, 1300],
8 | [1, 90, 4400, 1500],
9 | [1, 100, 4800, 1600],
10 | [1, 110, 5300, 1800],
11 |
12 | [2, 20, 2000, 700],
13 | [2, 25, 2400, null],
14 | [2, 30, 2900, 1000],
15 | [2, 40, 3900, 1300],
16 | [2, 50, 4800, 1600],
17 | [2, 60, 5800, 2000],
18 | [2, 70, 6800, 2300],
19 | [2, 80, 7700, 2600],
20 | [2, 90, 8700, 2900],
21 | [2, 100, 9600, 3200],
22 | [2, 110, 10600, 3600],
23 |
24 | [3, 20, 3900, 1300],
25 | [3, 25, 4800, 1600],
26 | [3, 30, 5800, 2000],
27 | [3, 40, 7700, 2600],
28 | [3, 50, 9600, 3200],
29 | [3, 60, 11600, 3900],
30 | [3, 70, 12000, 4000],
31 | [3, 80, 12000, 4000],
32 | [3, 90, 12000, 4000],
33 | [3, 100, 12000, 4000],
34 | [3, 110, 12000, 4000],
35 |
36 | [4, 20, 7700, 2600],
37 | [4, 25, 9600, 3200],
38 | [4, 30, 11600, 3900],
39 | [4, 40, 12000, 4000],
40 | [4, 50, 12000, 4000],
41 | [4, 60, 12000, 4000],
42 | [4, 70, 12000, 4000],
43 | [4, 80, 12000, 4000],
44 | [4, 90, 12000, 4000],
45 | [4, 100, 12000, 4000],
46 | [4, 110, 12000, 4000],
47 |
48 | [5, 20, 12000, 4000],
49 | [5, 25, 12000, 4000],
50 | [5, 30, 12000, 4000],
51 | [5, 40, 12000, 4000],
52 | [5, 50, 12000, 4000],
53 | [5, 60, 12000, 4000],
54 | [5, 70, 12000, 4000],
55 | [5, 80, 12000, 4000],
56 | [5, 90, 12000, 4000],
57 | [5, 100, 12000, 4000],
58 | [5, 110, 12000, 4000],
59 |
60 | [6, 20, 18000, 6000],
61 | [6, 25, 18000, 6000],
62 | [6, 30, 18000, 6000],
63 | [6, 40, 18000, 6000],
64 | [6, 50, 18000, 6000],
65 | [6, 60, 18000, 6000],
66 | [6, 70, 18000, 6000],
67 | [6, 80, 18000, 6000],
68 | [6, 90, 18000, 6000],
69 | [6, 100, 18000, 6000],
70 | [6, 110, 18000, 6000],
71 |
72 | [7, 20, 18000, 6000],
73 | [7, 25, 18000, 6000],
74 | [7, 30, 18000, 6000],
75 | [7, 40, 18000, 6000],
76 | [7, 50, 18000, 6000],
77 | [7, 60, 18000, 6000],
78 | [7, 70, 18000, 6000],
79 | [7, 80, 18000, 6000],
80 | [7, 90, 18000, 6000],
81 | [7, 100, 18000, 6000],
82 | [7, 110, 18000, 6000],
83 |
84 | [8, 20, 24000, 8000],
85 | [8, 25, 24000, 8000],
86 | [8, 30, 24000, 8000],
87 | [8, 40, 24000, 8000],
88 | [8, 50, 24000, 8000],
89 | [8, 60, 24000, 8000],
90 | [8, 70, 24000, 8000],
91 | [8, 80, 24000, 8000],
92 | [8, 90, 24000, 8000],
93 | [8, 100, 24000, 8000],
94 | [8, 110, 24000, 8000],
95 |
96 | [9, 20, 24000, 8000],
97 | [9, 25, 24000, 8000],
98 | [9, 30, 24000, 8000],
99 | [9, 40, 24000, 8000],
100 | [9, 50, 24000, 8000],
101 | [9, 60, 24000, 8000],
102 | [9, 70, 24000, 8000],
103 | [9, 80, 24000, 8000],
104 | [9, 90, 24000, 8000],
105 | [9, 100, 24000, 8000],
106 | [9, 110, 24000, 8000],
107 |
108 | [10, 20, 24000, 8000],
109 | [10, 25, 24000, 8000],
110 | [10, 30, 24000, 8000],
111 | [10, 40, 24000, 8000],
112 | [10, 50, 24000, 8000],
113 | [10, 60, 24000, 8000],
114 | [10, 70, 24000, 8000],
115 | [10, 80, 24000, 8000],
116 | [10, 90, 24000, 8000],
117 | [10, 100, 24000, 8000],
118 | [10, 110, 24000, 8000],
119 |
120 | [11, 20, 36000, 12000],
121 | [11, 25, 36000, 12000],
122 | [11, 30, 36000, 12000],
123 | [11, 40, 36000, 12000],
124 | [11, 50, 36000, 12000],
125 | [11, 60, 36000, 12000],
126 | [11, 70, 36000, 12000],
127 | [11, 80, 36000, 12000],
128 | [11, 90, 36000, 12000],
129 | [11, 100, 36000, 12000],
130 | [11, 110, 36000, 12000],
131 |
132 | [12, 20, 36000, 12000],
133 | [12, 25, 36000, 12000],
134 | [12, 30, 36000, 12000],
135 | [12, 40, 36000, 12000],
136 | [12, 50, 36000, 12000],
137 | [12, 60, 36000, 12000],
138 | [12, 70, 36000, 12000],
139 | [12, 80, 36000, 12000],
140 | [12, 90, 36000, 12000],
141 | [12, 100, 36000, 12000],
142 | [12, 110, 36000, 12000]
143 | ]
--------------------------------------------------------------------------------
/tests/integration/ema/ema-point-calculator.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import EmaPointCalculator from '@/core/ema/ema-point-calculator'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new EmaPointCalculator()
6 |
7 | test('kazoe yakuman should count as sanbaiman', () => {
8 | const hand = new Hand({ seatWind: 'east', winningType: 'tsumo' })
9 | const result = sut.calculate(hand, null, 13)
10 | expect(result).toStrictEqual({ nonDealer: 12000 })
11 | })
12 |
13 | test('kiriage mangan should not be active', () => {
14 | const hand = new Hand({ seatWind: 'east', winningType: 'ron' })
15 | const result = sut.calculate(hand, 30, 4)
16 | expect(result).toStrictEqual({ discard: 11600 })
17 | })
18 |
--------------------------------------------------------------------------------
/tests/integration/wrc/wrc-point-calculator.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import WrcPointCalculator from '@/core/wrc/wrc-point-calculator'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new WrcPointCalculator()
6 |
7 | test('kazoe yakuman should count as sanbaiman', () => {
8 | const hand = new Hand({ seatWind: 'east', winningType: 'tsumo' })
9 | const result = sut.calculate(hand, null, 13)
10 | expect(result).toStrictEqual({ nonDealer: 12000 })
11 | })
12 |
13 | test('kiriage mangan should be active', () => {
14 | const hand = new Hand({ seatWind: 'east', winningType: 'ron' })
15 | const result = sut.calculate(hand, 30, 4)
16 | expect(result).toStrictEqual({ discard: 12000 })
17 | })
18 |
--------------------------------------------------------------------------------
/tests/unit/fu-calculation/fu-calculator.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import FuCalculator from '@/core/fu-calculation/fu-calculator'
3 | import Hand from '@/core/hand'
4 |
5 | const hand = new Hand() // just a placeholder because rule checkers will be mocked anyway
6 |
7 | test('trying to instantiate the calculator without rules parameter should throw an error', () => {
8 | /* eslint-disable no-new */
9 | expect(() => { new FuCalculator() }).toThrow('rules parameter is required')
10 | })
11 |
12 | test('should support rule returning nothing', () => {
13 | const rules = [{ check () {} }]
14 | const result = new FuCalculator(rules).calculate(hand)
15 | expect(result.details).toStrictEqual([])
16 | })
17 |
18 | test('should support rule returning a fu info object', () => {
19 | const rules = [{ check: () => ({ key: 'test', fuValue: 1, quantity: 1 }) }]
20 | const result = new FuCalculator(rules).calculate(hand)
21 | expect(result.details).toStrictEqual([{ key: 'test', fuValue: 1, quantity: 1 }])
22 | })
23 |
24 | test('should support rule returning an array of fu info', () => {
25 | const rules = [
26 | { check: () => ({ key: 'test', fuValue: 1, quantity: 1 }) },
27 | { check: () => ({ key: 'test 2', fuValue: 2, quantity: 5 }) }
28 | ]
29 |
30 | const result = new FuCalculator(rules).calculate(hand)
31 |
32 | expect(result.details).toStrictEqual([
33 | { key: 'test', fuValue: 1, quantity: 1 },
34 | { key: 'test 2', fuValue: 2, quantity: 5 }
35 | ])
36 | })
37 |
38 | test('should return the total fu value of the hand', () => {
39 | const rules = [
40 | { check: () => ({ key: 'test', fuValue: 2, quantity: 1 }) },
41 | { check: () => ({ key: 'test 2', fuValue: 2, quantity: 4 }) }
42 | ]
43 |
44 | const result = new FuCalculator(rules).calculate(hand)
45 |
46 | expect(result.total).toBe(10)
47 | })
48 |
49 | test('should round up to the tens the total fu value of the hand', () => {
50 | const rules = [
51 | { check: () => ({ key: 'test', fuValue: 2, quantity: 1 }) },
52 | { check: () => ({ key: 'test 2', fuValue: 2, quantity: 5 }) }
53 | ]
54 |
55 | const result = new FuCalculator(rules).calculate(hand)
56 |
57 | expect(result.total).toBe(20)
58 | })
59 |
60 | describe('given a rule want to stop the process', () => {
61 | const rules = [
62 | {
63 | check (hand, context) {
64 | context.stop = true
65 | return { key: 'test', fuValue: 1, quantity: 1 }
66 | }
67 | },
68 | { check: () => ({ key: 'test 2', fuValue: 1, quantity: 1 }) }
69 | ]
70 |
71 | test('should not evaluate the next rules', () => {
72 | const result = new FuCalculator(rules).calculate(hand)
73 | expect(result.details).toStrictEqual([{ key: 'test', fuValue: 1, quantity: 1 }])
74 | })
75 | })
76 |
77 | describe('given a rule want to disable the rounding', () => {
78 | const rules = [
79 | {
80 | check (hand, context) {
81 | context.rounding = false
82 | return { key: 'test', fuValue: 25, quantity: 1 }
83 | }
84 | }
85 | ]
86 |
87 | test('should not round up to the tens the total fu value of the hand', () => {
88 | const result = new FuCalculator(rules).calculate(hand)
89 | expect(result.total).toBe(25)
90 | })
91 | })
92 |
--------------------------------------------------------------------------------
/tests/unit/fu-calculation/rules/chiitoitsu-fu-rule.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import ChiitoitsuRule from '@/core/fu-calculation/rules/chiitoitsu-fu-rule'
3 | import Hand from '@/core/hand'
4 |
5 | const hand = new Hand() // just a placeholder because the yaku checker will be mocked anyway
6 |
7 | function makeDefaultFuCalculationContext () {
8 | return { stop: false, rounding: true }
9 | }
10 |
11 | test('trying to instantiate the rule without chiitoitsuYakuPattern option should throw an error', () => {
12 | const expectedError = 'chiitoitsuYakuPattern is required'
13 | /* eslint-disable no-new */
14 | expect(() => { new ChiitoitsuRule() }).toThrow(expectedError)
15 | /* eslint-disable no-new */
16 | expect(() => { new ChiitoitsuRule({}) }).toThrow(expectedError)
17 | /* eslint-disable no-new */
18 | expect(() => { new ChiitoitsuRule({ fuValue: 50 }) }).toThrow(expectedError)
19 | })
20 |
21 | describe('given the hand is not considered a chiitoitsu (seven pairs)', () => {
22 | const mockChiitoitsuYakuPattern = { check: () => false }
23 |
24 | test('should not return any fu info', () => {
25 | const context = makeDefaultFuCalculationContext()
26 | const result = new ChiitoitsuRule({ chiitoitsuYakuPattern: mockChiitoitsuYakuPattern }).check(hand, context)
27 | expect(result).toBeUndefined()
28 | })
29 |
30 | test('should not stop the process', () => {
31 | const context = makeDefaultFuCalculationContext()
32 | new ChiitoitsuRule({ chiitoitsuYakuPattern: mockChiitoitsuYakuPattern }).check(hand, context)
33 | expect(context.stop).toBe(false)
34 | })
35 |
36 | test('should not disable the rounding', () => {
37 | const context = makeDefaultFuCalculationContext()
38 | new ChiitoitsuRule({ chiitoitsuYakuPattern: mockChiitoitsuYakuPattern }).check(hand, context)
39 | expect(context.rounding).toBe(true)
40 | })
41 | })
42 |
43 | describe('given the hand is considered a chiitoitsu (seven pairs)', () => {
44 | const mockChiitoitsuYakuPattern = { check: () => true }
45 |
46 | test('should return 25 fu by default', () => {
47 | const context = makeDefaultFuCalculationContext()
48 | const result = new ChiitoitsuRule({ chiitoitsuYakuPattern: mockChiitoitsuYakuPattern }).check(hand, context)
49 | expect(result).toStrictEqual({ key: 'chiitoitsu', fuValue: 25, quantity: 1 })
50 | })
51 |
52 | test('should return the fu value corresponding to the rule option', () => {
53 | const context = makeDefaultFuCalculationContext()
54 | const result = new ChiitoitsuRule({ chiitoitsuYakuPattern: mockChiitoitsuYakuPattern, fuValue: 50 }).check(hand, context)
55 | expect(result).toStrictEqual({ key: 'chiitoitsu', fuValue: 50, quantity: 1 })
56 | })
57 |
58 | test('should stop the process', () => {
59 | const context = makeDefaultFuCalculationContext()
60 | new ChiitoitsuRule({ chiitoitsuYakuPattern: mockChiitoitsuYakuPattern }).check(hand, context)
61 | expect(context.stop).toBe(true)
62 | })
63 |
64 | test('should disable the rounding', () => {
65 | const context = makeDefaultFuCalculationContext()
66 | new ChiitoitsuRule({ chiitoitsuYakuPattern: mockChiitoitsuYakuPattern }).check(hand, context)
67 | expect(context.rounding).toBe(false)
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/tests/unit/fu-calculation/rules/closed-ron-fu-rule.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import ClosedRonRule from '@/core/fu-calculation/rules/closed-ron-fu-rule'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair } from '@/core/combination-classes'
5 | import { DotTile } from '@/core/tile-classes'
6 |
7 | describe('given the hand is not concealed', () => {
8 | const hand = new Hand({
9 | concealedCombinations: [
10 | new Triplet(new DotTile(1)),
11 | new Triplet(new DotTile(2)),
12 | new Triplet(new DotTile(3)),
13 | new Pair(new DotTile(4))
14 | ],
15 | openCombinations: [
16 | new Triplet(new DotTile(5))
17 | ]
18 | })
19 |
20 | test('should not return any fu info', () => {
21 | const result = new ClosedRonRule().check(hand)
22 | expect(result).toBeUndefined()
23 | })
24 | })
25 |
26 | describe('given the hand was not won by ron (discard)', () => {
27 | const hand = new Hand({
28 | concealedCombinations: [
29 | new Triplet(new DotTile(1)),
30 | new Triplet(new DotTile(2)),
31 | new Triplet(new DotTile(3)),
32 | new Triplet(new DotTile(5)),
33 | new Pair(new DotTile(4))
34 | ],
35 | winningType: 'tsumo'
36 | })
37 |
38 | test('should not return any fu info', () => {
39 | const result = new ClosedRonRule().check(hand)
40 | expect(result).toBeUndefined()
41 | })
42 | })
43 |
44 | describe('given the hand is concealed and was won by ron (discard)', () => {
45 | const hand = new Hand({
46 | concealedCombinations: [
47 | new Triplet(new DotTile(1)),
48 | new Triplet(new DotTile(2)),
49 | new Triplet(new DotTile(3)),
50 | new Triplet(new DotTile(5)),
51 | new Pair(new DotTile(4))
52 | ],
53 | winningType: 'ron'
54 | })
55 |
56 | test('should return 10 fu', () => {
57 | const result = new ClosedRonRule().check(hand)
58 | expect(result).toStrictEqual({ key: 'closed ron', fuValue: 10, quantity: 1 })
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/tests/unit/fu-calculation/rules/tsumo-fu-rule.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import TsumoRule from '@/core/fu-calculation/rules/tsumo-fu-rule'
3 | import Hand from '@/core/hand'
4 | import { Pair, Triplet } from '@/core/combination-classes'
5 | import { DotTile, DragonTile, WindTile } from '@/core/tile-classes'
6 |
7 | function makeDefaultHand (winningType) {
8 | return new Hand({
9 | concealedCombinations: [
10 | new Triplet(new DragonTile('green')),
11 | new Triplet(new DragonTile('red')),
12 | new Triplet(new DragonTile('white')),
13 | new Triplet(new WindTile('east')),
14 | new Pair(new DotTile(5))
15 | ],
16 | winningType
17 | })
18 | }
19 |
20 | describe('given the hand was won by tsumo (self-draw)', () => {
21 | const hand = makeDefaultHand('tsumo')
22 |
23 | test('should return 2 fu', () => {
24 | const result = new TsumoRule().check(hand)
25 | expect(result).toStrictEqual({ key: 'tsumo', fuValue: 2, quantity: 1 })
26 | })
27 | })
28 |
29 | describe('given the hand was won by ron', () => {
30 | const hand = makeDefaultHand('ron')
31 |
32 | test('should not return any fu info', () => {
33 | const result = new TsumoRule().check(hand)
34 | expect(result).toBeUndefined()
35 | })
36 | })
37 |
38 | describe('given the hand contains no excluded yaku and was won by tsumo', () => {
39 | const hand = makeDefaultHand('tsumo')
40 |
41 | const mockExcludedYakuPatterns = [
42 | { check: () => false },
43 | { check: () => false }
44 | ]
45 |
46 | test('should return 2 fu', () => {
47 | const result = new TsumoRule({ excludedYakuPatterns: mockExcludedYakuPatterns }).check(hand)
48 | expect(result).toStrictEqual({ key: 'tsumo', fuValue: 2, quantity: 1 })
49 | })
50 | })
51 |
52 | describe('given the hand contains one or more excluded yaku', () => {
53 | const hand = makeDefaultHand('tsumo')
54 |
55 | const mockExcludedYakuPatterns = [
56 | { check: () => false },
57 | { check: () => true }
58 | ]
59 |
60 | test('should not return any fu info', () => {
61 | const result = new TsumoRule({ excludedYakuPatterns: mockExcludedYakuPatterns }).check(hand)
62 | expect(result).toBeUndefined()
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/tests/unit/fu-calculation/rules/wait-fu-rule.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import WaitRule from '@/core/fu-calculation/rules/wait-fu-rule'
3 | import Hand from '@/core/hand'
4 | import { Pair, Triplet, Sequence } from '@/core/combination-classes'
5 | import { DotTile } from '@/core/tile-classes'
6 |
7 | describe('given the wait is a single wait', () => {
8 | const hand = new Hand({
9 | concealedCombinations: [
10 | new Pair(new DotTile(1)),
11 | new Triplet(new DotTile(2)),
12 | new Triplet(new DotTile(3)),
13 | new Triplet(new DotTile(4)),
14 | new Triplet(new DotTile(5))
15 | ],
16 | winningCombinationIndex: 0,
17 | winningTileIndex: 0
18 | })
19 |
20 | test('should return 2 fu', () => {
21 | const result = new WaitRule().check(hand)
22 | expect(result).toStrictEqual({ key: 'wait', fuValue: 2, quantity: 1 })
23 | })
24 | })
25 |
26 | describe('given the wait is a edge wait', () => {
27 | const hand = new Hand({
28 | concealedCombinations: [
29 | new Pair(new DotTile(1)),
30 | new Sequence(new DotTile(1), new DotTile(2), new DotTile(3)),
31 | new Triplet(new DotTile(3)),
32 | new Triplet(new DotTile(4)),
33 | new Triplet(new DotTile(5))
34 | ],
35 | winningCombinationIndex: 0,
36 | winningTileIndex: 2
37 | })
38 |
39 | test('should return 2 fu', () => {
40 | const result = new WaitRule().check(hand)
41 | expect(result).toStrictEqual({ key: 'wait', fuValue: 2, quantity: 1 })
42 | })
43 | })
44 |
45 | describe('given the wait is a closed wait', () => {
46 | const hand = new Hand({
47 | concealedCombinations: [
48 | new Pair(new DotTile(1)),
49 | new Sequence(new DotTile(1), new DotTile(2), new DotTile(3)),
50 | new Triplet(new DotTile(3)),
51 | new Triplet(new DotTile(4)),
52 | new Triplet(new DotTile(5))
53 | ],
54 | winningCombinationIndex: 0,
55 | winningTileIndex: 1
56 | })
57 |
58 | test('should return 2 fu', () => {
59 | const result = new WaitRule().check(hand)
60 | expect(result).toStrictEqual({ key: 'wait', fuValue: 2, quantity: 1 })
61 | })
62 | })
63 |
64 | describe('given the wait is not a single, edge or closed wait', () => {
65 | const hand = new Hand({
66 | concealedCombinations: [
67 | new Pair(new DotTile(1)),
68 | new Triplet(new DotTile(2)),
69 | new Triplet(new DotTile(3)),
70 | new Triplet(new DotTile(4)),
71 | new Triplet(new DotTile(5))
72 | ],
73 | winningCombinationIndex: 1,
74 | winningTileIndex: 0
75 | })
76 |
77 | test('should not return any fu info', () => {
78 | const result = new WaitRule().check(hand)
79 | expect(result).toBeUndefined()
80 | })
81 | })
82 |
--------------------------------------------------------------------------------
/tests/unit/fu-calculation/rules/winning-fu-rule.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import WinningFuRule from '@/core/fu-calculation/rules/winning-fu-rule'
3 |
4 | test('should return 20 fu', () => {
5 | const result = new WinningFuRule().check()
6 | expect(result).toStrictEqual({ key: 'win', fuValue: 20, quantity: 1 })
7 | })
8 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/chankan-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import ChankanYaku from '@/core/han-calculation/yakus/chankan-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new ChankanYaku()
6 |
7 | test('chankan (Robbing the quad) valid hand', () => {
8 | const hand = new Hand({ yakus: ['chankan'] })
9 | expect(sut.check(hand)).toStrictEqual({ key: 'chankan', hanValue: 1, yakumanValue: 0 })
10 | })
11 |
12 | test('chankan (Robbing the quad) invalid hand', () => {
13 | const hand = new Hand()
14 | expect(sut.check(hand)).toBeUndefined()
15 | })
16 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/chanta-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import ChantaYaku from '@/core/han-calculation/yakus/chanta-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence } from '@/core/combination-classes'
5 | import { DotTile, BambooTile, DragonTile } from '@/core/tile-classes'
6 |
7 | const sut = new ChantaYaku()
8 |
9 | const validConcealedHand = new Hand({
10 | concealedCombinations: [
11 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
12 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
13 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
14 | new Triplet(new DragonTile('red')),
15 | new Pair(new DotTile(1))
16 | ]
17 | })
18 |
19 | test('chanta (outside hand) valid concealed hand', () => {
20 | expect(sut.check(validConcealedHand)).toStrictEqual({ key: 'chanta', hanValue: 2, yakumanValue: 0 })
21 | })
22 |
23 | const validOpenHand = new Hand({
24 | concealedCombinations: [
25 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
26 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
27 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3))
28 | ],
29 | openCombinations: [
30 | new Triplet(new DragonTile('red')),
31 | new Pair(new DotTile(1))
32 | ]
33 | })
34 |
35 | test('chanta (outside hand) valid open hand', () => {
36 | expect(sut.check(validOpenHand)).toStrictEqual({ key: 'chanta', hanValue: 1, yakumanValue: 0 })
37 | })
38 |
39 | const invalidHandWithoutChii = new Hand({
40 | concealedCombinations: [
41 | new Triplet(new BambooTile(1)),
42 | new Triplet(new BambooTile(9)),
43 | new Triplet(new DotTile(1)),
44 | new Triplet(new DragonTile('red')),
45 | new Pair(new DotTile(9))
46 | ]
47 | })
48 |
49 | test('chanta (outside hand) invalid without one sequence', () => {
50 | expect(sut.check(invalidHandWithoutChii)).toBeUndefined()
51 | })
52 |
53 | // check for terminals
54 | const invalidHandWithSetWithoutTerminalOrHonor = new Hand({
55 | concealedCombinations: [
56 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
57 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
58 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
59 | new Triplet(new DragonTile('red')),
60 | new Pair(new DotTile(1))
61 | ]
62 | })
63 |
64 | test('chanta (outside hand) invalid hand with set without terminal or honor', () => {
65 | expect(sut.check(invalidHandWithSetWithoutTerminalOrHonor)).toBeUndefined()
66 | })
67 |
68 | // check of honor tiles
69 | const invalidHandWithSetWithoutTerminalOrHonor2 = new Hand({
70 | concealedCombinations: [
71 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
72 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
73 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
74 | new Triplet(new DotTile(9)),
75 | new Pair(new DotTile(1))
76 | ]
77 | })
78 |
79 | test('chanta (outside hand) invalid hand with set without terminal or honor', () => {
80 | expect(sut.check(invalidHandWithSetWithoutTerminalOrHonor2)).toBeUndefined()
81 | })
82 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/chiihou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import ChiihouYaku from '@/core/han-calculation/yakus/chiihou-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | describe('given the hand contains the chiihou yaku', () => {
6 | const hand = new Hand({ yakus: ['chiihou'] })
7 |
8 | test('should be eligible for chiihou', () => {
9 | expect(new ChiihouYaku().check(hand)).toStrictEqual({ key: 'chiihou', hanValue: 0, yakumanValue: 1 })
10 | })
11 | })
12 |
13 | describe('given the hand does not contain the chiihou yaku', () => {
14 | const hand = new Hand()
15 |
16 | test('should not be eligible for chiihou', () => {
17 | expect(new ChiihouYaku().check(hand)).toBeUndefined()
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/chiitoitsu-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import ChiiToitsuYaku from '@/core/han-calculation/yakus/chiitoitsu-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair } from '@/core/combination-classes'
5 | import { BambooTile } from '@/core/tile-classes'
6 |
7 | const sut = new ChiiToitsuYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Pair(new BambooTile(1)),
12 | new Pair(new BambooTile(2)),
13 | new Pair(new BambooTile(3)),
14 | new Pair(new BambooTile(4)),
15 | new Pair(new BambooTile(5)),
16 | new Pair(new BambooTile(6)),
17 | new Pair(new BambooTile(7))
18 | ]
19 | })
20 | test('chiitoitsu (seven pairs) valid hand', () => {
21 | expect(sut.check(validHand)).toStrictEqual({ key: 'chiitoitsu', hanValue: 2, yakumanValue: 0 })
22 | })
23 |
24 | const invalidHandWithoutSevenPairs = new Hand({
25 | concealedCombinations: [
26 | new Triplet(new BambooTile(1)),
27 | new Triplet(new BambooTile(2)),
28 | new Triplet(new BambooTile(3)),
29 | new Triplet(new BambooTile(4)),
30 | new Pair(new BambooTile(5))
31 | ]
32 | })
33 | test('chiitoitsu (seven pairs) invalid hand without seven pairs', () => {
34 | expect(sut.check(invalidHandWithoutSevenPairs)).toBeUndefined()
35 | })
36 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/chinitsu-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import ChinitsuYaku from '@/core/han-calculation/yakus/chinitsu-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence } from '@/core/combination-classes'
5 | import { DotTile, BambooTile, DragonTile } from '@/core/tile-classes'
6 |
7 | const sut = new ChinitsuYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
12 | new Sequence(new BambooTile(6), new BambooTile(7), new BambooTile(8)),
13 | new Triplet(new BambooTile(1)),
14 | new Triplet(new BambooTile(9)),
15 | new Pair(new BambooTile(7))
16 | ]
17 | })
18 |
19 | test('chinitsu (full flush) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'chinitsu', hanValue: 6, yakumanValue: 0 })
21 | })
22 |
23 | const validHandWithOpenCombination = new Hand({
24 | concealedCombinations: [
25 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
26 | new Sequence(new BambooTile(6), new BambooTile(7), new BambooTile(8)),
27 | new Pair(new BambooTile(7))
28 | ],
29 | openCombinations: [
30 | new Triplet(new BambooTile(1)),
31 | new Triplet(new BambooTile(9))
32 | ]
33 | })
34 |
35 | test('chinitsu (full flush) valid hand with open combination', () => {
36 | expect(sut.check(validHandWithOpenCombination)).toStrictEqual({ key: 'chinitsu', hanValue: 5, yakumanValue: 0 })
37 | })
38 |
39 | const invalidHandWithHonorTiles = new Hand({
40 | concealedCombinations: [
41 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
42 | new Sequence(new BambooTile(6), new BambooTile(7), new BambooTile(8)),
43 | new Triplet(new BambooTile(1)),
44 | new Triplet(new BambooTile(9)),
45 | new Pair(new DragonTile('white'))
46 | ]
47 | })
48 |
49 | test('chinitsu (full flush) invalid hand with honor tile', () => {
50 | expect(sut.check(invalidHandWithHonorTiles)).toBeUndefined()
51 | })
52 |
53 | const invalidHandWithTwoSuit = new Hand({
54 | concealedCombinations: [
55 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
56 | new Sequence(new BambooTile(6), new BambooTile(7), new BambooTile(8)),
57 | new Triplet(new BambooTile(1)),
58 | new Triplet(new DotTile(9)),
59 | new Pair(new BambooTile(7))
60 | ]
61 | })
62 |
63 | test('chinitsu (full flush) invalid hand with two suit', () => {
64 | expect(sut.check(invalidHandWithTwoSuit)).toBeUndefined()
65 | })
66 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/chinroutou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import ChinroutouYaku from '@/core/han-calculation/yakus/chinroutou-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair } from '@/core/combination-classes'
5 | import { DotTile, BambooTile, DragonTile, WindTile, CharacterTile } from '@/core/tile-classes'
6 |
7 | test('chinroutou (all terminals) valid hand with only teminal tiles', () => {
8 | const hand = new Hand({
9 | concealedCombinations: [
10 | new Pair(new DotTile(1)),
11 | new Triplet(new BambooTile(1)),
12 | new Triplet(new BambooTile(9)),
13 | new Triplet(new CharacterTile(1))
14 | ],
15 | openCombinations: [
16 | new Triplet(new CharacterTile(9))
17 | ]
18 | })
19 |
20 | expect(new ChinroutouYaku().check(hand)).toStrictEqual({ key: 'chinroutou', hanValue: 0, yakumanValue: 1 })
21 | })
22 |
23 | test('chinroutou (all terminals) invalid hand because it contains a dragon tile', () => {
24 | const hand = new Hand({
25 | concealedCombinations: [
26 | new Pair(new DragonTile('green')),
27 | new Triplet(new BambooTile(1)),
28 | new Triplet(new BambooTile(9)),
29 | new Triplet(new CharacterTile(1))
30 | ],
31 | openCombinations: [
32 | new Triplet(new CharacterTile(9))
33 | ]
34 | })
35 |
36 | expect(new ChinroutouYaku().check(hand)).toBeUndefined()
37 | })
38 |
39 | test('chinroutou (all terminals) invalid hand because it contains a wind tile', () => {
40 | const hand = new Hand({
41 | concealedCombinations: [
42 | new Pair(new WindTile('east')),
43 | new Triplet(new BambooTile(1)),
44 | new Triplet(new BambooTile(9)),
45 | new Triplet(new CharacterTile(1))
46 | ],
47 | openCombinations: [
48 | new Triplet(new CharacterTile(9))
49 | ]
50 | })
51 |
52 | expect(new ChinroutouYaku().check(hand)).toBeUndefined()
53 | })
54 |
55 | test('chinroutou (all terminals) invalid hand because it contains a non terminal numbered tile', () => {
56 | const hand = new Hand({
57 | concealedCombinations: [
58 | new Pair(new DotTile(2)),
59 | new Triplet(new BambooTile(1)),
60 | new Triplet(new BambooTile(9)),
61 | new Triplet(new CharacterTile(1))
62 | ],
63 | openCombinations: [
64 | new Triplet(new CharacterTile(9))
65 | ]
66 | })
67 |
68 | expect(new ChinroutouYaku().check(hand)).toBeUndefined()
69 | })
70 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/daisangen-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import DaisangenYaku from '@/core/han-calculation/yakus/daisangen-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Quad } from '@/core/combination-classes'
5 | import { DotTile, DragonTile } from '@/core/tile-classes'
6 |
7 | test('daisangen (big three dragons) valid hand with a triplet of each dragon tile', () => {
8 | const hand = new Hand({
9 | concealedCombinations: [
10 | new Pair(new DotTile(1)),
11 | new Triplet(new DragonTile('red')),
12 | new Triplet(new DragonTile('green')),
13 | new Triplet(new DotTile(2))
14 | ],
15 | openCombinations: [
16 | new Triplet(new DragonTile('white'))
17 | ]
18 | })
19 |
20 | expect(new DaisangenYaku().check(hand)).toStrictEqual({ key: 'daisangen', hanValue: 0, yakumanValue: 1 })
21 | })
22 |
23 | test('daisangen (big three dragons) valid hand with a quad of each dragon tile', () => {
24 | const hand = new Hand({
25 | concealedCombinations: [
26 | new Pair(new DotTile(1)),
27 | new Quad(new DragonTile('red')),
28 | new Quad(new DragonTile('green')),
29 | new Triplet(new DotTile(2))
30 | ],
31 | openCombinations: [
32 | new Quad(new DragonTile('white'))
33 | ]
34 | })
35 |
36 | expect(new DaisangenYaku().check(hand)).toStrictEqual({ key: 'daisangen', hanValue: 0, yakumanValue: 1 })
37 | })
38 |
39 | test('daisangen (big three dragons) valid hand with a mix of triplet and quad of each dragon tile', () => {
40 | const hand = new Hand({
41 | concealedCombinations: [
42 | new Pair(new DotTile(1)),
43 | new Quad(new DragonTile('red')),
44 | new Triplet(new DragonTile('green')),
45 | new Triplet(new DotTile(2))
46 | ],
47 | openCombinations: [
48 | new Quad(new DragonTile('white'))
49 | ]
50 | })
51 |
52 | expect(new DaisangenYaku().check(hand)).toStrictEqual({ key: 'daisangen', hanValue: 0, yakumanValue: 1 })
53 | })
54 |
55 | test('daisangen (big three dragons) invalid hand because there is no triplet or quad of white dragon', () => {
56 | const hand = new Hand({
57 | concealedCombinations: [
58 | new Pair(new DotTile(1)),
59 | new Triplet(new DragonTile('red')),
60 | new Triplet(new DragonTile('green')),
61 | new Triplet(new DotTile(3))
62 | ],
63 | openCombinations: [
64 | new Triplet(new DotTile(2))
65 | ]
66 | })
67 |
68 | expect(new DaisangenYaku().check(hand)).toBeUndefined()
69 | })
70 |
71 | test('daisangen (big three dragons) invalid hand because there is no triplet or quad of red dragon', () => {
72 | const hand = new Hand({
73 | concealedCombinations: [
74 | new Pair(new DotTile(1)),
75 | new Triplet(new DragonTile('white')),
76 | new Triplet(new DragonTile('green')),
77 | new Triplet(new DotTile(3))
78 | ],
79 | openCombinations: [
80 | new Triplet(new DotTile(2))
81 | ]
82 | })
83 |
84 | expect(new DaisangenYaku().check(hand)).toBeUndefined()
85 | })
86 |
87 | test('daisangen (big three dragons) invalid hand because there is no triplet or quad of green dragon', () => {
88 | const hand = new Hand({
89 | concealedCombinations: [
90 | new Pair(new DotTile(1)),
91 | new Triplet(new DragonTile('white')),
92 | new Triplet(new DragonTile('red')),
93 | new Triplet(new DotTile(3))
94 | ],
95 | openCombinations: [
96 | new Triplet(new DotTile(2))
97 | ]
98 | })
99 |
100 | expect(new DaisangenYaku().check(hand)).toBeUndefined()
101 | })
102 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/daisuushii-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import DaisuushiiYaku from '@/core/han-calculation/yakus/daisuushii-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Quad } from '@/core/combination-classes'
5 | import { DotTile, WindTile } from '@/core/tile-classes'
6 |
7 | test('daisuushii (big four winds) valid hand with a triplet of each wind tile', () => {
8 | const hand = new Hand({
9 | concealedCombinations: [
10 | new Pair(new DotTile(1)),
11 | new Triplet(new WindTile('east')),
12 | new Triplet(new WindTile('south')),
13 | new Triplet(new WindTile('west'))
14 | ],
15 | openCombinations: [
16 | new Triplet(new WindTile('north'))
17 | ]
18 | })
19 |
20 | expect(new DaisuushiiYaku().check(hand)).toStrictEqual({ key: 'daisuushii', hanValue: 0, yakumanValue: 1 })
21 | expect(new DaisuushiiYaku({ allowDoubleYakuman: true }).check(hand)).toStrictEqual({ key: 'daisuushii', hanValue: 0, yakumanValue: 2 })
22 | })
23 |
24 | test('daisuushii (big four winds) valid hand with a quad of each wind tile', () => {
25 | const hand = new Hand({
26 | concealedCombinations: [
27 | new Pair(new DotTile(1)),
28 | new Quad(new WindTile('east')),
29 | new Quad(new WindTile('south')),
30 | new Quad(new WindTile('west'))
31 | ],
32 | openCombinations: [
33 | new Quad(new WindTile('north'))
34 | ]
35 | })
36 |
37 | expect(new DaisuushiiYaku().check(hand)).toStrictEqual({ key: 'daisuushii', hanValue: 0, yakumanValue: 1 })
38 | expect(new DaisuushiiYaku({ allowDoubleYakuman: true }).check(hand)).toStrictEqual({ key: 'daisuushii', hanValue: 0, yakumanValue: 2 })
39 | })
40 |
41 | test('daisuushii (big four winds) valid hand with a mix of four triplet/quad of wind tile', () => {
42 | const hand = new Hand({
43 | concealedCombinations: [
44 | new Pair(new DotTile(1)),
45 | new Quad(new WindTile('east')),
46 | new Triplet(new WindTile('south')),
47 | new Quad(new WindTile('west'))
48 | ],
49 | openCombinations: [
50 | new Triplet(new WindTile('north'))
51 | ]
52 | })
53 |
54 | expect(new DaisuushiiYaku().check(hand)).toStrictEqual({ key: 'daisuushii', hanValue: 0, yakumanValue: 1 })
55 | expect(new DaisuushiiYaku({ allowDoubleYakuman: true }).check(hand)).toStrictEqual({ key: 'daisuushii', hanValue: 0, yakumanValue: 2 })
56 | })
57 |
58 | test('daisuushii (big four winds) invalid hand because it does not contains four triplet/quad of wind tile', () => {
59 | const hand = new Hand({
60 | concealedCombinations: [
61 | new Pair(new WindTile('east')),
62 | new Quad(new DotTile(1)),
63 | new Triplet(new WindTile('south')),
64 | new Quad(new WindTile('west'))
65 | ],
66 | openCombinations: [
67 | new Triplet(new WindTile('north'))
68 | ]
69 | })
70 |
71 | expect(new DaisuushiiYaku().check(hand)).toBeUndefined()
72 | })
73 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/double-riichi-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import DoubleRiichiYaku from '@/core/han-calculation/yakus/double-riichi-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new DoubleRiichiYaku()
6 |
7 | describe('given the hand contains the double riichi yaku', () => {
8 | const hand = new Hand({ yakus: ['double riichi'] })
9 |
10 | test('should be eligible for double riichi', () => {
11 | expect(sut.check(hand)).toStrictEqual({ key: 'double riichi', hanValue: 1, yakumanValue: 0 })
12 | })
13 | })
14 |
15 | describe('given the hand does not contain the double riichi yaku', () => {
16 | const hand = new Hand()
17 |
18 | test('should not be eligible for double riichi', () => {
19 | expect(sut.check(hand)).toBeUndefined()
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/haitei-raoyue-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import HaiteiRaoyueYaku from '@/core/han-calculation/yakus/haitei-raoyue-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new HaiteiRaoyueYaku()
6 |
7 | describe('given the hand contains the haitei raoyue yaku', () => {
8 | const hand = new Hand({ yakus: ['haitei raoyue'] })
9 |
10 | test('should be eligible for haitei raoyue', () => {
11 | expect(sut.check(hand)).toStrictEqual({ key: 'haitei raoyue', hanValue: 1, yakumanValue: 0 })
12 | })
13 | })
14 |
15 | describe('given the hand does not contain the haitei raoyue yaku', () => {
16 | const hand = new Hand()
17 |
18 | test('should not be eligible for haitei raoyue', () => {
19 | expect(sut.check(hand)).toBeUndefined()
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/honitsu-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import HonitsuYaku from '@/core/han-calculation/yakus/honitsu-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair } from '@/core/combination-classes'
5 | import { DotTile, BambooTile, DragonTile } from '@/core/tile-classes'
6 |
7 | const sut = new HonitsuYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Pair(new DragonTile('green')),
12 | new Triplet(new BambooTile(1)),
13 | new Triplet(new BambooTile(2)),
14 | new Triplet(new BambooTile(3)),
15 | new Triplet(new BambooTile(4))
16 | ]
17 | })
18 | test('honitsu (half flush) valid hand', () => {
19 | expect(sut.check(validHand)).toStrictEqual({ key: 'honitsu', hanValue: 3, yakumanValue: 0 })
20 | })
21 |
22 | const validHandWithOpenCombination = new Hand({
23 | concealedCombinations: [
24 | new Pair(new DragonTile('green')),
25 | new Triplet(new BambooTile(3)),
26 | new Triplet(new BambooTile(4))
27 | ],
28 | openCombinations: [
29 | new Triplet(new BambooTile(1)),
30 | new Triplet(new BambooTile(2))
31 | ]
32 | })
33 | test('honitsu (half flush) valid hand with open combination', () => {
34 | expect(sut.check(validHandWithOpenCombination)).toStrictEqual({ key: 'honitsu', hanValue: 2, yakumanValue: 0 })
35 | })
36 |
37 | const invalidHandWithoutHonorTile = new Hand({
38 | concealedCombinations: [
39 | new Triplet(new BambooTile(9)),
40 | new Triplet(new BambooTile(1)),
41 | new Triplet(new BambooTile(2)),
42 | new Triplet(new BambooTile(3)),
43 | new Pair(new BambooTile(4))
44 | ]
45 | })
46 | test('honitsu (half flush) invalid hand without honor tile', () => {
47 | expect(sut.check(invalidHandWithoutHonorTile)).toBeUndefined()
48 | })
49 |
50 | const invalidHandWithTwoSuit = new Hand({
51 | concealedCombinations: [
52 | new Pair(new DragonTile('green')),
53 | new Triplet(new BambooTile(1)),
54 | new Triplet(new BambooTile(2)),
55 | new Triplet(new BambooTile(3)),
56 | new Triplet(new DotTile(4))
57 | ]
58 | })
59 | test('honitsu (half flush) invalid hand with two suit', () => {
60 | expect(sut.check(invalidHandWithTwoSuit)).toBeUndefined()
61 | })
62 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/honroutou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import HonroutouYaku from '@/core/han-calculation/yakus/honroutou-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair } from '@/core/combination-classes'
5 | import { DotTile, BambooTile, DragonTile, WindTile } from '@/core/tile-classes'
6 |
7 | const sut = new HonroutouYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Triplet(new DragonTile('green')),
12 | new Triplet(new DragonTile('white')),
13 | new Triplet(new WindTile('east')),
14 | new Triplet(new DotTile(1)),
15 | new Pair(new BambooTile(9))
16 | ]
17 | })
18 |
19 | test('honroutou (all terminals & honors) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'honroutou', hanValue: 2, yakumanValue: 0 })
21 | })
22 |
23 | const invalidHandWithTileNotHonorOrTerminal = new Hand({
24 | concealedCombinations: [
25 | new Triplet(new DragonTile('green')),
26 | new Triplet(new DragonTile('white')),
27 | new Triplet(new WindTile('east')),
28 | new Triplet(new DotTile(1)),
29 | new Pair(new BambooTile(5))
30 | ]
31 | })
32 |
33 | test('honroutou (all terminals & honors) invvalid hand with tiles not honor or terminal', () => {
34 | expect(sut.check(invalidHandWithTileNotHonorOrTerminal)).toBeUndefined()
35 | })
36 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/houtei-raoyui-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import HouteiRaoyuiYaku from '@/core/han-calculation/yakus/houtei-raoyui-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new HouteiRaoyuiYaku()
6 |
7 | describe('given the hand contains the houtei raoyui yaku', () => {
8 | const hand = new Hand({ yakus: ['houtei raoyui'] })
9 |
10 | test('should be eligible for houtei raoyui', () => {
11 | expect(sut.check(hand)).toStrictEqual({ key: 'houtei raoyui', hanValue: 1, yakumanValue: 0 })
12 | })
13 | })
14 |
15 | describe('given the hand does not contain the houtei raoyui yaku', () => {
16 | const hand = new Hand()
17 |
18 | test('should not be eligible for houtei raoyui', () => {
19 | expect(sut.check(hand)).toBeUndefined()
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/iipeikou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import IipeikouYaku from '@/core/han-calculation/yakus/iipeikou-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence } from '@/core/combination-classes'
5 | import { DotTile, CharacterTile, BambooTile } from '@/core/tile-classes'
6 |
7 | const sut = new IipeikouYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
12 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
13 | new Sequence(new CharacterTile(3), new CharacterTile(4), new CharacterTile(5)),
14 | new Triplet(new DotTile(3)),
15 | new Pair(new DotTile(7))
16 | ]
17 | })
18 |
19 | test('iipeikou (pure double sequence) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'iipeikou', hanValue: 1, yakumanValue: 0 })
21 | })
22 |
23 | // test with a open hand
24 | const invalidOpenHand = new Hand({
25 | concealedCombinations: [
26 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
27 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
28 | new Sequence(new CharacterTile(3), new CharacterTile(4), new CharacterTile(5))
29 | ],
30 | openCombinations: [
31 | new Triplet(new DotTile(3)),
32 | new Pair(new DotTile(7))
33 | ]
34 | })
35 |
36 | test('iipeikou (pure double sequence) invalid with a open hand', () => {
37 | expect(sut.check(invalidOpenHand)).toBeUndefined()
38 | })
39 |
40 | // test with different number
41 | const invalidHandWithoutTwoIdenticalChii1 = new Hand({
42 | concealedCombinations: [
43 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
44 | new Sequence(new BambooTile(3), new BambooTile(4), new BambooTile(5)),
45 | new Sequence(new CharacterTile(3), new CharacterTile(4), new CharacterTile(5)),
46 | new Triplet(new DotTile(3)),
47 | new Pair(new DotTile(7))
48 | ]
49 | })
50 |
51 | test('iipeikou (pure double sequence) invalid hand without two identical sequence', () => {
52 | expect(sut.check(invalidHandWithoutTwoIdenticalChii1)).toBeUndefined()
53 | })
54 |
55 | // test with different suit
56 | const invalidHandWithoutTwoIdenticalChii2 = new Hand({
57 | concealedCombinations: [
58 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
59 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
60 | new Sequence(new CharacterTile(3), new CharacterTile(4), new CharacterTile(5)),
61 | new Triplet(new DotTile(3)),
62 | new Pair(new DotTile(7))
63 | ]
64 | })
65 |
66 | test('iipeikou (pure double sequence) invalid hand without two identical sequence', () => {
67 | expect(sut.check(invalidHandWithoutTwoIdenticalChii2)).toBeUndefined()
68 | })
69 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/ippatsu-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import IppatsuYaku from '@/core/han-calculation/yakus/ippatsu-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new IppatsuYaku()
6 |
7 | describe('given the hand contains the ippatsu yaku', () => {
8 | const hand = new Hand({ yakus: ['ippatsu'] })
9 |
10 | test('should be eligible for ippatsu', () => {
11 | expect(sut.check(hand)).toStrictEqual({ key: 'ippatsu', hanValue: 1, yakumanValue: 0 })
12 | })
13 | })
14 |
15 | describe('given the hand does not contain the ippatsu yaku', () => {
16 | const hand = new Hand()
17 |
18 | test('should not be eligible for ippatsu', () => {
19 | expect(sut.check(hand)).toBeUndefined()
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/ittsuu-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import IttsuuYaku from '@/core/han-calculation/yakus/ittsuu-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence } from '@/core/combination-classes'
5 | import { DotTile, BambooTile } from '@/core/tile-classes'
6 |
7 | const sut = new IttsuuYaku()
8 |
9 | const validConcealedHand = new Hand({
10 | concealedCombinations: [
11 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
12 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
13 | new Sequence(new BambooTile(4), new BambooTile(5), new BambooTile(6)),
14 | new Triplet(new DotTile(3)),
15 | new Pair(new DotTile(7))
16 | ]
17 | })
18 |
19 | test('ittsuu (pure straight) valid concealed hand', () => {
20 | expect(sut.check(validConcealedHand)).toStrictEqual({ key: 'ittsuu', hanValue: 2, yakumanValue: 0 })
21 | })
22 |
23 | const validOpenHand = new Hand({
24 | concealedCombinations: [
25 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
26 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
27 | new Sequence(new BambooTile(4), new BambooTile(5), new BambooTile(6))
28 | ],
29 | openCombinations: [
30 | new Triplet(new DotTile(3)),
31 | new Pair(new DotTile(7))
32 | ]
33 | })
34 |
35 | test('ittsuu (pure straight) valid open hand', () => {
36 | expect(sut.check(validOpenHand)).toStrictEqual({ key: 'ittsuu', hanValue: 1, yakumanValue: 0 })
37 | })
38 |
39 | const invalidHandWithoutOneToNineNumber = new Hand({
40 | concealedCombinations: [
41 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
42 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
43 | new Sequence(new BambooTile(4), new BambooTile(5), new BambooTile(6)),
44 | new Triplet(new DotTile(3)),
45 | new Pair(new DotTile(7))
46 | ]
47 | })
48 |
49 | test('ittsuu (pure straight) invalid hand without one to nine number', () => {
50 | expect(sut.check(invalidHandWithoutOneToNineNumber)).toBeUndefined()
51 | })
52 |
53 | const invalidHandWithTwoSuit = new Hand({
54 | concealedCombinations: [
55 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
56 | new Sequence(new DotTile(7), new DotTile(8), new DotTile(9)),
57 | new Sequence(new BambooTile(4), new BambooTile(5), new BambooTile(6)),
58 | new Triplet(new DotTile(3)),
59 | new Pair(new DotTile(7))
60 | ]
61 | })
62 |
63 | test('ittsuu (pure straight) valid hand', () => {
64 | expect(sut.check(invalidHandWithTwoSuit)).toBeUndefined()
65 | })
66 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/junchan-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import JunchanYaku from '@/core/han-calculation/yakus/junchan-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence } from '@/core/combination-classes'
5 | import { DotTile, CharacterTile, BambooTile } from '@/core/tile-classes'
6 |
7 | const sut = new JunchanYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
12 | new Triplet(new BambooTile(9)),
13 | new Triplet(new DotTile(1)),
14 | new Triplet(new DotTile(9)),
15 | new Pair(new CharacterTile(1))
16 | ]
17 | })
18 |
19 | test('junchan (terminals in all sets) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'junchan', hanValue: 3, yakumanValue: 0 })
21 | })
22 |
23 | const invalidHandWithoutChii = new Hand({
24 | concealedCombinations: [
25 | new Triplet(new BambooTile(1)),
26 | new Triplet(new BambooTile(9)),
27 | new Triplet(new DotTile(1)),
28 | new Triplet(new DotTile(9)),
29 | new Pair(new CharacterTile(1))
30 | ]
31 | })
32 |
33 | test('junchan (terminals in all sets) invalid hand without sequence', () => {
34 | expect(sut.check(invalidHandWithoutChii)).toBeUndefined()
35 | })
36 |
37 | const invalidHandWithSetWithoutTerminal = new Hand({
38 | concealedCombinations: [
39 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
40 | new Triplet(new BambooTile(8)),
41 | new Triplet(new DotTile(1)),
42 | new Triplet(new DotTile(9)),
43 | new Pair(new CharacterTile(1))
44 | ]
45 | })
46 |
47 | test('junchan (terminals in all sets) with set without terminal', () => {
48 | expect(sut.check(invalidHandWithSetWithoutTerminal)).toBeUndefined()
49 | })
50 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/menzen-tsumo-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import MenzenTsumoYaku from '@/core/han-calculation/yakus/menzen-tsumo-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence } from '@/core/combination-classes'
5 | import { DotTile, BambooTile, DragonTile } from '@/core/tile-classes'
6 |
7 | const sut = new MenzenTsumoYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
12 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
13 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
14 | new Triplet(new DragonTile('red')),
15 | new Pair(new DotTile(1))
16 | ],
17 | winningType: 'tsumo'
18 | })
19 |
20 | test('Menzen Tsumo (Fully Concealed Hand) valid hand', () => {
21 | expect(sut.check(validHand)).toStrictEqual({ key: 'menzen tsumo', hanValue: 1, yakumanValue: 0 })
22 | })
23 |
24 | const invalidHandWithRon = new Hand({
25 | concealedCombinations: [
26 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
27 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
28 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
29 | new Triplet(new DragonTile('red')),
30 | new Pair(new DotTile(1))
31 | ],
32 | winningType: 'ron'
33 | })
34 |
35 | test('Menzen Tsumo (Fully Concealed Hand) invalid hand win by ron', () => {
36 | expect(sut.check(invalidHandWithRon)).toBeUndefined()
37 | })
38 |
39 | const invalidHandNotConcealed = new Hand({
40 | concealedCombinations: [
41 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
42 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
43 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
44 | new Pair(new DotTile(1))
45 | ],
46 | openCombinations: [
47 | new Triplet(new DragonTile('red'))
48 | ],
49 | winningType: 'tsumo'
50 | })
51 |
52 | test('Menzen Tsumo (Fully Concealed Hand) invalid hand with open combination', () => {
53 | expect(sut.check(invalidHandNotConcealed)).toBeUndefined()
54 | })
55 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/open-riichi-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import OpenRiichiYaku from '@/core/han-calculation/yakus/open-riichi-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | describe('given the hand contains the riichi yaku', () => {
6 | test('should be worth 1 han with the hand is won by tsumo (self-draw)', () => {
7 | const hand = new Hand({ yakus: ['riichi', 'open riichi'], winningType: 'tsumo' })
8 | expect(new OpenRiichiYaku().check(hand)).toStrictEqual({ key: 'open riichi', hanValue: 1, yakumanValue: 0 })
9 | })
10 |
11 | test('should be worth 1 han if the hand was won by ron (discard) and the yaku option for ronAsYakuman is false (default)', () => {
12 | const hand = new Hand({ yakus: ['riichi', 'open riichi'], winningType: 'ron' })
13 | expect(new OpenRiichiYaku().check(hand)).toStrictEqual({ key: 'open riichi', hanValue: 1, yakumanValue: 0 })
14 | })
15 |
16 | test('should be worth 1 yakuman if the hand was won by ron (discard) and the yaku option for ronAsYakuman is true', () => {
17 | const hand = new Hand({ yakus: ['riichi', 'open riichi'], winningType: 'ron' })
18 | expect(new OpenRiichiYaku({ ronAsYakuman: true }).check(hand)).toStrictEqual({ key: 'open riichi', hanValue: 0, yakumanValue: 1 })
19 | })
20 | })
21 |
22 | describe('given the hand does not contain the open riichi yaku', () => {
23 | const hand = new Hand()
24 |
25 | test('should not be eligible for open riichi', () => {
26 | expect(new OpenRiichiYaku().check(hand)).toBeUndefined()
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/renhou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import RenhouYaku from '@/core/han-calculation/yakus/renhou-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | describe('given the hand contains the renhou yaku', () => {
6 | const hand = new Hand({ yakus: ['renhou'] })
7 |
8 | test('should be worth 5 han by default', () => {
9 | expect(new RenhouYaku().check(hand)).toStrictEqual({ key: 'renhou', hanValue: 5, yakumanValue: 0 })
10 | })
11 |
12 | test('shoud be worth the amount of han configured with the yaku options', () => {
13 | expect(new RenhouYaku({ hanValue: 4, yakumanValue: 0 }).check(hand)).toStrictEqual({ key: 'renhou', hanValue: 4, yakumanValue: 0 })
14 | })
15 |
16 | test('shoud be worth the amount of yakuman configured with the yaku options', () => {
17 | expect(new RenhouYaku({ hanValue: 0, yakumanValue: 1 }).check(hand)).toStrictEqual({ key: 'renhou', hanValue: 0, yakumanValue: 1 })
18 | })
19 | })
20 |
21 | describe('given the hand does not contain the renhou yaku', () => {
22 | const hand = new Hand()
23 |
24 | test('should not be eligible for renhou', () => {
25 | expect(new RenhouYaku().check(hand)).toBeUndefined()
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/riichi-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import RiichiYaku from '@/core/han-calculation/yakus/riichi-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new RiichiYaku()
6 |
7 | describe('given the hand contains the riichi yaku', () => {
8 | const hand = new Hand({ yakus: ['riichi'] })
9 |
10 | test('should be eligible for riichi', () => {
11 | expect(sut.check(hand)).toStrictEqual({ key: 'riichi', hanValue: 1, yakumanValue: 0 })
12 | })
13 | })
14 |
15 | describe('given the hand does not contain the riichi yaku', () => {
16 | const hand = new Hand()
17 |
18 | test('should not be eligible for riichi', () => {
19 | expect(sut.check(hand)).toBeUndefined()
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/rinshan-kaihou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import RinshanKaihouYaku from '@/core/han-calculation/yakus/rinshan-kaihou-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new RinshanKaihouYaku()
6 |
7 | describe('given the hand contains the rinshan kaihou yaku', () => {
8 | const hand = new Hand({ yakus: ['rinshan kaihou'] })
9 |
10 | test('should be eligible for rinshan kaihou', () => {
11 | expect(sut.check(hand)).toStrictEqual({ key: 'rinshan kaihou', hanValue: 1, yakumanValue: 0 })
12 | })
13 | })
14 |
15 | describe('given the hand does not contain the rinshan kaihou yaku', () => {
16 | const hand = new Hand()
17 |
18 | test('should not be eligible for rinshan kaihou', () => {
19 | expect(sut.check(hand)).toBeUndefined()
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/ryanpeikou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import RyanpeikouYaku from '@/core/han-calculation/yakus/ryanpeikou-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence } from '@/core/combination-classes'
5 | import { DotTile, BambooTile } from '@/core/tile-classes'
6 |
7 | const sut = new RyanpeikouYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
12 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
13 | new Sequence(new DotTile(7), new DotTile(8), new DotTile(9)),
14 | new Sequence(new DotTile(7), new DotTile(8), new DotTile(9)),
15 | new Pair(new DotTile(1))
16 | ]
17 | })
18 |
19 | test('ryan peikou (twice pure double chiis) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'ryanpeikou', hanValue: 3, yakumanValue: 0 })
21 | })
22 |
23 | describe('given the hand have a valid open ryan peikou (twice pure double chiis)', () => {
24 | const validOpenHand = new Hand({
25 | concealedCombinations: [
26 | new Pair(new DotTile(1)),
27 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3))
28 | ],
29 | openCombinations: [
30 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
31 | new Sequence(new DotTile(7), new DotTile(8), new DotTile(9)),
32 | new Sequence(new DotTile(7), new DotTile(8), new DotTile(9))
33 | ]
34 | })
35 |
36 | test('should not return any han by default', () => {
37 | expect(sut.check(validOpenHand)).toBeUndefined()
38 | })
39 |
40 | test('should return 2 han if the yaku option allow open hand', () => {
41 | expect(new RyanpeikouYaku({ allowOpen: true }).check(validOpenHand)).toStrictEqual({ key: 'ryanpeikou', hanValue: 2, yakumanValue: 0 })
42 | })
43 | })
44 |
45 | const invalidHandWitoutTwoPairOfChii = new Hand({
46 | concealedCombinations: [
47 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
48 | new Sequence(new BambooTile(1), new BambooTile(2), new BambooTile(3)),
49 | new Sequence(new DotTile(7), new DotTile(8), new DotTile(9)),
50 | new Sequence(new DotTile(1), new DotTile(2), new DotTile(3)),
51 | new Pair(new DotTile(1))
52 | ]
53 | })
54 |
55 | test('ryan peikou (twice pure double chiis) invalid hand without two pair of sequence', () => {
56 | expect(sut.check(invalidHandWitoutTwoPairOfChii)).toBeUndefined()
57 | })
58 |
59 | const invalidHandWithoutChii = new Hand({
60 | concealedCombinations: [
61 | new Triplet(new BambooTile(1)),
62 | new Triplet(new BambooTile(2)),
63 | new Triplet(new DotTile(7)),
64 | new Triplet(new DotTile(1)),
65 | new Pair(new DotTile(1))
66 | ]
67 | })
68 |
69 | test('ryan peikou (twice pure double chiis) invalid hand without sequence', () => {
70 | expect(sut.check(invalidHandWithoutChii)).toBeUndefined()
71 | })
72 |
73 | const invalidHandWithThreeIdenticalChii = new Hand({
74 | concealedCombinations: [
75 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
76 | new Pair(new BambooTile(4)),
77 | new Sequence(new BambooTile(6), new BambooTile(7), new BambooTile(8)),
78 | new Sequence(new BambooTile(6), new BambooTile(7), new BambooTile(8)),
79 | new Sequence(new BambooTile(6), new BambooTile(7), new BambooTile(8))
80 | ]
81 | })
82 |
83 | test('ryan peikou (twice pure double chiis) invalid hand with three identical sequences', () => {
84 | expect(sut.check(invalidHandWithThreeIdenticalChii)).toBeUndefined()
85 | })
86 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/sanankou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import SanankouYaku from '@/core/han-calculation/yakus/sanankou-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Quad } from '@/core/combination-classes'
5 | import { DotTile } from '@/core/tile-classes'
6 |
7 | const sut = new SanankouYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Triplet(new DotTile(1)),
12 | new Triplet(new DotTile(2)),
13 | new Triplet(new DotTile(3)),
14 | new Triplet(new DotTile(4)),
15 | new Pair(new DotTile(5))
16 | ]
17 | })
18 | test('Sanankou (3 concealed pons) valid hand', () => {
19 | expect(sut.check(validHand)).toStrictEqual({ key: 'sanankou', hanValue: 2, yakumanValue: 0 })
20 | })
21 |
22 | const validOpenHand = new Hand({
23 | concealedCombinations: [
24 | new Triplet(new DotTile(1)),
25 | new Triplet(new DotTile(2)),
26 | new Triplet(new DotTile(3)),
27 | new Pair(new DotTile(5))
28 | ],
29 | openCombinations: [
30 | new Triplet(new DotTile(4))
31 | ]
32 | })
33 | test('Sanankou (3 concealed pons) valid open hand', () => {
34 | expect(sut.check(validOpenHand)).toStrictEqual({ key: 'sanankou', hanValue: 2, yakumanValue: 0 })
35 | })
36 |
37 | const validHandWithKan = new Hand({
38 | concealedCombinations: [
39 | new Triplet(new DotTile(1)),
40 | new Triplet(new DotTile(2)),
41 | new Quad(new DotTile(3)),
42 | new Triplet(new DotTile(4)),
43 | new Pair(new DotTile(5))
44 | ]
45 | })
46 | test('Sanankou (3 concealed pons) valid hand with quad', () => {
47 | expect(sut.check(validHandWithKan)).toStrictEqual({ key: 'sanankou', hanValue: 2, yakumanValue: 0 })
48 | })
49 |
50 | const invalidHandWithLessThanThreeConcealedPon = new Hand({
51 | concealedCombinations: [
52 | new Triplet(new DotTile(3)),
53 | new Pair(new DotTile(5))
54 | ],
55 | openCombinations: [
56 | new Triplet(new DotTile(1)),
57 | new Triplet(new DotTile(2)),
58 | new Triplet(new DotTile(4))
59 | ]
60 | })
61 | test('Sanankou (3 concealed pons) invalid hand with less than 3 concealed pons', () => {
62 | expect(sut.check(invalidHandWithLessThanThreeConcealedPon)).toBeUndefined()
63 | })
64 |
65 | describe('given the third ankou (concealed triplet) is also the winning tile', () => {
66 | const concealedCombinations = [
67 | new Triplet(new DotTile(1)),
68 | new Triplet(new DotTile(2)),
69 | new Triplet(new DotTile(3)),
70 | new Pair(new DotTile(4))
71 | ]
72 | const openCombinations = [
73 | new Triplet(new DotTile(5))
74 | ]
75 |
76 | test('should be valid for a san ankou yaku if the hand was won by tsumo (self-draw)', () => {
77 | const hand = new Hand({ concealedCombinations, openCombinations, winningType: 'tsumo', winningCombinationIndex: 0, winningTileIndex: 0 })
78 | expect(sut.check(hand)).toStrictEqual({ key: 'sanankou', hanValue: 2, yakumanValue: 0 })
79 | })
80 |
81 | test('should not be eligible for san ankou yaku if the hand was won by ron (discard)', () => {
82 | const hand = new Hand({ concealedCombinations, openCombinations, winningType: 'ron', winningCombinationIndex: 0, winningTileIndex: 0 })
83 | expect(sut.check(hand)).toBeUndefined()
84 | })
85 | })
86 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/sankantsu-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import SankantsuYaku from '@/core/han-calculation/yakus/sankantsu-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Quad } from '@/core/combination-classes'
5 | import { DotTile } from '@/core/tile-classes'
6 |
7 | const sut = new SankantsuYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Quad(new DotTile(1)),
12 | new Quad(new DotTile(2)),
13 | new Quad(new DotTile(3)),
14 | new Triplet(new DotTile(4)),
15 | new Pair(new DotTile(6))
16 | ]
17 | })
18 |
19 | test('Sankantsu (3 kans) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'sankantsu', hanValue: 2, yakumanValue: 0 })
21 | })
22 |
23 | const validHandWithOpenKan = new Hand({
24 | concealedCombinations: [
25 | new Quad(new DotTile(3)),
26 | new Triplet(new DotTile(4)),
27 | new Pair(new DotTile(6))
28 | ],
29 | openCombinations: [
30 | new Quad(new DotTile(1)),
31 | new Quad(new DotTile(2))
32 | ]
33 | })
34 |
35 | test('Sankantsu (3 kans) valid hand with open quad', () => {
36 | expect(sut.check(validHandWithOpenKan)).toStrictEqual({ key: 'sankantsu', hanValue: 2, yakumanValue: 0 })
37 | })
38 |
39 | const invalidHandWithLessThanThreeKan = new Hand({
40 | concealedCombinations: [
41 | new Quad(new DotTile(1)),
42 | new Quad(new DotTile(2)),
43 | new Triplet(new DotTile(3)),
44 | new Triplet(new DotTile(4)),
45 | new Pair(new DotTile(6))
46 | ]
47 | })
48 |
49 | test('Sankantsu (3 kans) invalid hand with less than three quad', () => {
50 | expect(sut.check(invalidHandWithLessThanThreeKan)).toBeUndefined()
51 | })
52 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/sanshoku-doujun-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import SanshokuDoujunYaku from '@/core/han-calculation/yakus/sanshoku-doujun-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence } from '@/core/combination-classes'
5 | import { DotTile, CharacterTile, BambooTile } from '@/core/tile-classes'
6 |
7 | const sut = new SanshokuDoujunYaku()
8 |
9 | const validConcealedHand = new Hand({
10 | concealedCombinations: [
11 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
12 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
13 | new Sequence(new CharacterTile(2), new CharacterTile(3), new CharacterTile(4)),
14 | new Triplet(new DotTile(3)),
15 | new Pair(new DotTile(7))
16 | ]
17 | })
18 |
19 | test('sanshoku doujun (mixed triple sequence) valid concealed hand', () => {
20 | expect(sut.check(validConcealedHand)).toStrictEqual({ key: 'sanshoku doujun', hanValue: 2, yakumanValue: 0 })
21 | })
22 |
23 | const validOpenHand = new Hand({
24 | concealedCombinations: [
25 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
26 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
27 | new Sequence(new CharacterTile(2), new CharacterTile(3), new CharacterTile(4))
28 | ],
29 | openCombinations: [
30 | new Triplet(new DotTile(3)),
31 | new Pair(new DotTile(7))
32 | ]
33 | })
34 |
35 | test('sanshoku doujun (mixed triple sequence) valid open hand', () => {
36 | expect(sut.check(validOpenHand)).toStrictEqual({ key: 'sanshoku doujun', hanValue: 1, yakumanValue: 0 })
37 | })
38 |
39 | const invalidHandWithOnlyTwoChiiOfTheSameValue = new Hand({
40 | concealedCombinations: [
41 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
42 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
43 | new Triplet(new CharacterTile(2)),
44 | new Triplet(new DotTile(3)),
45 | new Pair(new DotTile(7))
46 | ]
47 | })
48 |
49 | test('sanshoku doujun (mixed triple sequence) invalid hand with only two sequence of the same value', () => {
50 | expect(sut.check(invalidHandWithOnlyTwoChiiOfTheSameValue)).toBeUndefined()
51 | })
52 |
53 | const invalidHandWithoutThreeDifferentSuit = new Hand({
54 | concealedCombinations: [
55 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
56 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
57 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
58 | new Triplet(new DotTile(3)),
59 | new Pair(new DotTile(7))
60 | ]
61 | })
62 |
63 | test('sanshoku doujun (mixed triple sequence) invalid hand without three different suit', () => {
64 | expect(sut.check(invalidHandWithoutThreeDifferentSuit)).toBeUndefined()
65 | })
66 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/sanshoku-doukou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import SanshokuDokouYaku from '@/core/han-calculation/yakus/sanshoku-doukou-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair } from '@/core/combination-classes'
5 | import { DotTile, CharacterTile, BambooTile } from '@/core/tile-classes'
6 |
7 | const sut = new SanshokuDokouYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Triplet(new BambooTile(1)),
12 | new Triplet(new DotTile(1)),
13 | new Triplet(new CharacterTile(1)),
14 | new Triplet(new DotTile(3)),
15 | new Pair(new DotTile(7))
16 | ]
17 | })
18 |
19 | test('sanshoku dokou (triple triplet) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'sanshoku doukou', hanValue: 2, yakumanValue: 0 })
21 | })
22 |
23 | const invalidHandWithoutThreeSamePon = new Hand({
24 | concealedCombinations: [
25 | new Triplet(new BambooTile(1)),
26 | new Triplet(new DotTile(1)),
27 | new Triplet(new CharacterTile(2)),
28 | new Triplet(new DotTile(3)),
29 | new Pair(new DotTile(7))
30 | ]
31 | })
32 |
33 | test('sanshoku dokou (triple triplet) invalid hand without three same triplet', () => {
34 | expect(sut.check(invalidHandWithoutThreeSamePon)).toBeUndefined()
35 | })
36 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/shousangen-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import ShousangenYaku from '@/core/han-calculation/yakus/shousangen-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Quad } from '@/core/combination-classes'
5 | import { BambooTile, DragonTile, WindTile } from '@/core/tile-classes'
6 |
7 | const sut = new ShousangenYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Triplet(new DragonTile('red')),
12 | new Triplet(new DragonTile('green')),
13 | new Pair(new DragonTile('white')),
14 | new Triplet(new BambooTile(1)),
15 | new Triplet(new BambooTile(2))
16 | ]
17 | })
18 |
19 | test('shousangen (little three dragons) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'shousangen', hanValue: 2, yakumanValue: 0 })
21 | })
22 |
23 | const validHandWithKan = new Hand({
24 | concealedCombinations: [
25 | new Triplet(new DragonTile('red')),
26 | new Quad(new DragonTile('green')),
27 | new Pair(new DragonTile('white')),
28 | new Triplet(new BambooTile(1)),
29 | new Triplet(new BambooTile(2))
30 | ]
31 | })
32 |
33 | test('shousangen (little three dragons) valid hand with quad', () => {
34 | expect(sut.check(validHandWithKan)).toStrictEqual({ key: 'shousangen', hanValue: 2, yakumanValue: 0 })
35 | })
36 |
37 | const invalidHandWithoutDragonPair = new Hand({
38 | concealedCombinations: [
39 | new Triplet(new DragonTile('red')),
40 | new Triplet(new DragonTile('green')),
41 | new Pair(new WindTile('east')),
42 | new Triplet(new BambooTile(1)),
43 | new Triplet(new BambooTile(2))
44 | ]
45 | })
46 |
47 | test('shousangen (little three dragons) invalid hand without dragon pair', () => {
48 | expect(sut.check(invalidHandWithoutDragonPair)).toBeUndefined()
49 | })
50 |
51 | const invalidHandWithoutTwoDragonPon = new Hand({
52 | concealedCombinations: [
53 | new Triplet(new DragonTile('red')),
54 | new Triplet(new WindTile('north')),
55 | new Pair(new DragonTile('white')),
56 | new Triplet(new BambooTile(1)),
57 | new Triplet(new BambooTile(2))
58 | ]
59 | })
60 |
61 | test('shousangen (little three dragons) invalid hand without two dragon triplet', () => {
62 | expect(sut.check(invalidHandWithoutTwoDragonPon)).toBeUndefined()
63 | })
64 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/shousuushii-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import ShousuushiiYaku from '@/core/han-calculation/yakus/shousuushii-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Quad } from '@/core/combination-classes'
5 | import { DotTile, WindTile } from '@/core/tile-classes'
6 |
7 | test('shousuushii (little four winds) valid hand with three triplet of wind tile and a pair of wind tile', () => {
8 | const hand = new Hand({
9 | concealedCombinations: [
10 | new Pair(new WindTile('east')),
11 | new Triplet(new WindTile('south')),
12 | new Triplet(new WindTile('west')),
13 | new Triplet(new DotTile(1))
14 | ],
15 | openCombinations: [
16 | new Triplet(new WindTile('north'))
17 | ]
18 | })
19 |
20 | expect(new ShousuushiiYaku().check(hand)).toStrictEqual({ key: 'shousuushii', hanValue: 0, yakumanValue: 1 })
21 | })
22 |
23 | test('shousuushii (little four winds) valid hand with three quad of wind tile and a pair of wind tile', () => {
24 | const hand = new Hand({
25 | concealedCombinations: [
26 | new Pair(new WindTile('south')),
27 | new Quad(new WindTile('west')),
28 | new Quad(new WindTile('north')),
29 | new Triplet(new DotTile(1))
30 | ],
31 | openCombinations: [
32 | new Quad(new WindTile('east'))
33 | ]
34 | })
35 |
36 | expect(new ShousuushiiYaku().check(hand)).toStrictEqual({ key: 'shousuushii', hanValue: 0, yakumanValue: 1 })
37 | })
38 |
39 | test('shousuushii (little four winds) valid hand with a mix of three triplet/quad of wind tile and a pair of wind tile', () => {
40 | const hand = new Hand({
41 | concealedCombinations: [
42 | new Pair(new WindTile('west')),
43 | new Quad(new WindTile('north')),
44 | new Triplet(new WindTile('east')),
45 | new Triplet(new DotTile(1))
46 | ],
47 | openCombinations: [
48 | new Quad(new WindTile('south'))
49 | ]
50 | })
51 |
52 | expect(new ShousuushiiYaku().check(hand)).toStrictEqual({ key: 'shousuushii', hanValue: 0, yakumanValue: 1 })
53 | })
54 |
55 | test('shousuushii (little four winds) invalid hand because it contains four triplet/quad of wind tiles (big four winds)', () => {
56 | const hand = new Hand({
57 | concealedCombinations: [
58 | new Pair(new DotTile(1)),
59 | new Triplet(new WindTile('north')),
60 | new Triplet(new WindTile('east')),
61 | new Triplet(new WindTile('west'))
62 | ],
63 | openCombinations: [
64 | new Quad(new WindTile('south'))
65 | ]
66 | })
67 |
68 | expect(new ShousuushiiYaku().check(hand)).toBeUndefined()
69 | })
70 |
71 | test('shousuushii (little four winds) invalid hand because it doesn\'t contains a wind pair', () => {
72 | const hand = new Hand({
73 | concealedCombinations: [
74 | new Pair(new DotTile(1)),
75 | new Triplet(new WindTile('north')),
76 | new Triplet(new WindTile('east')),
77 | new Triplet(new DotTile(2))
78 | ],
79 | openCombinations: [
80 | new Quad(new WindTile('south'))
81 | ]
82 | })
83 |
84 | expect(new ShousuushiiYaku().check(hand)).toBeUndefined()
85 | })
86 |
87 | test('shousuushii (little four winds) invalid hand because it doesn\'t contains three triplet/quad of wind tile', () => {
88 | const hand = new Hand({
89 | concealedCombinations: [
90 | new Pair(new WindTile('west')),
91 | new Triplet(new WindTile('north')),
92 | new Triplet(new WindTile('east')),
93 | new Triplet(new DotTile(2))
94 | ],
95 | openCombinations: [
96 | new Quad(new DotTile(1))
97 | ]
98 | })
99 |
100 | expect(new ShousuushiiYaku().check(hand)).toBeUndefined()
101 | })
102 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/suukantsu-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import SuukantsuYaku from '@/core/han-calculation/yakus/suukantsu-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence, Quad } from '@/core/combination-classes'
5 | import { DotTile } from '@/core/tile-classes'
6 |
7 | const sut = new SuukantsuYaku()
8 |
9 | test('suukantsu (four kans) invalid hand because there is a triplet', () => {
10 | const hand = new Hand({
11 | concealedCombinations: [
12 | new Pair(new DotTile(5)),
13 | new Quad(new DotTile(1)),
14 | new Quad(new DotTile(2)),
15 | new Quad(new DotTile(3)),
16 | new Triplet(new DotTile(4))
17 | ]
18 | })
19 |
20 | expect(sut.check(hand)).toBeUndefined()
21 | })
22 |
23 | test('suukantsu (four kans) invalid hand because there is a sequence', () => {
24 | const hand = new Hand({
25 | concealedCombinations: [
26 | new Pair(new DotTile(5)),
27 | new Quad(new DotTile(1)),
28 | new Quad(new DotTile(2)),
29 | new Quad(new DotTile(3)),
30 | new Sequence(new DotTile(4))
31 | ]
32 | })
33 |
34 | expect(sut.check(hand)).toBeUndefined()
35 | })
36 |
37 | test('suukantsu (four kans) valid hand with four open/concealed kans', () => {
38 | const hand = new Hand({
39 | concealedCombinations: [
40 | new Pair(new DotTile(5)),
41 | new Quad(new DotTile(1)),
42 | new Quad(new DotTile(2)),
43 | new Quad(new DotTile(3))
44 | ],
45 | openCombinations: [
46 | new Quad(new DotTile(4))
47 | ]
48 | })
49 |
50 | expect(sut.check(hand)).toStrictEqual({ key: 'suukantsu', hanValue: 0, yakumanValue: 1 })
51 | })
52 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/tanyao-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import TanyaouYaku from '@/core/han-calculation/yakus/tanyao-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence } from '@/core/combination-classes'
5 | import { DotTile, CharacterTile, BambooTile, DragonTile } from '@/core/tile-classes'
6 |
7 | const sut = new TanyaouYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
12 | new Sequence(new BambooTile(6), new BambooTile(7), new BambooTile(8)),
13 | new Sequence(new CharacterTile(3), new CharacterTile(4), new CharacterTile(5)),
14 | new Triplet(new DotTile(3)),
15 | new Pair(new DotTile(7))
16 | ]
17 | })
18 |
19 | test('tanyaou (all simples) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'tanyao', hanValue: 1, yakumanValue: 0 })
21 | })
22 |
23 | const invalidHandWithHonorTile = new Hand({
24 | concealedCombinations: [
25 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
26 | new Sequence(new BambooTile(6), new BambooTile(7), new BambooTile(8)),
27 | new Sequence(new CharacterTile(3), new CharacterTile(4), new CharacterTile(5)),
28 | new Triplet(new DragonTile('red')),
29 | new Pair(new DotTile(7))
30 | ]
31 | })
32 |
33 | test('tanyaou (all simples) invalid hand with honor tiles', () => {
34 | expect(sut.check(invalidHandWithHonorTile)).toBeUndefined()
35 | })
36 |
37 | const invalidHandWithTerminalTile = new Hand({
38 | concealedCombinations: [
39 | new Sequence(new BambooTile(2), new BambooTile(3), new BambooTile(4)),
40 | new Sequence(new BambooTile(7), new BambooTile(8), new BambooTile(9)),
41 | new Sequence(new CharacterTile(3), new CharacterTile(4), new CharacterTile(5)),
42 | new Triplet(new DotTile(3)),
43 | new Pair(new DotTile(7))
44 | ]
45 | })
46 |
47 | test('tanyaou (all simples) invalid hand with terminal tile', () => {
48 | expect(sut.check(invalidHandWithTerminalTile)).toBeUndefined()
49 | })
50 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/tenhou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import TenhouYaku from '@/core/han-calculation/yakus/tenhou-yaku'
3 | import Hand from '@/core/hand'
4 |
5 | const sut = new TenhouYaku()
6 |
7 | describe('given the hand contains the tenhou yaku', () => {
8 | const hand = new Hand({ yakus: ['tenhou'] })
9 |
10 | test('should be eligible for tenhou', () => {
11 | expect(sut.check(hand)).toStrictEqual({ key: 'tenhou', hanValue: 0, yakumanValue: 1 })
12 | })
13 | })
14 |
15 | describe('given the hand does not contain the tenhou yaku', () => {
16 | const hand = new Hand()
17 |
18 | test('should not be eligible for tenhou', () => {
19 | expect(sut.check(hand)).toBeUndefined()
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/toitoi-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import ToitoiYaku from '@/core/han-calculation/yakus/toitoi-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair, Sequence, Quad } from '@/core/combination-classes'
5 | import { DotTile, BambooTile } from '@/core/tile-classes'
6 |
7 | const sut = new ToitoiYaku()
8 |
9 | const validHand = new Hand({
10 | concealedCombinations: [
11 | new Triplet(new BambooTile(1)),
12 | new Triplet(new BambooTile(7)),
13 | new Triplet(new BambooTile(4)),
14 | new Triplet(new DotTile(3)),
15 | new Pair(new DotTile(7))
16 | ]
17 | })
18 |
19 | test('toitoi (all pons) valid hand', () => {
20 | expect(sut.check(validHand)).toStrictEqual({ key: 'toitoi', hanValue: 2, yakumanValue: 0 })
21 | })
22 |
23 | const validHandWithKan = new Hand({
24 | concealedCombinations: [
25 | new Triplet(new BambooTile(1)),
26 | new Triplet(new BambooTile(7)),
27 | new Triplet(new BambooTile(4)),
28 | new Quad(new DotTile(3)),
29 | new Pair(new DotTile(7))
30 | ]
31 | })
32 |
33 | test('toitoi (all pons) valid hand with quad', () => {
34 | expect(sut.check(validHandWithKan)).toStrictEqual({ key: 'toitoi', hanValue: 2, yakumanValue: 0 })
35 | })
36 |
37 | const invalidHandWithoutFourPon = new Hand({
38 | concealedCombinations: [
39 | new Triplet(new BambooTile(1)),
40 | new Triplet(new BambooTile(7)),
41 | new Triplet(new BambooTile(4)),
42 | new Sequence(new DotTile(3), new DotTile(4), new DotTile(5)),
43 | new Pair(new DotTile(7))
44 | ]
45 | })
46 |
47 | test('toitoi (all pons) invalid hand without four triplet', () => {
48 | expect(sut.check(invalidHandWithoutFourPon)).toBeUndefined()
49 | })
50 |
--------------------------------------------------------------------------------
/tests/unit/han-calculation/yakus/tsuuiisou-yaku.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import TsuuiisouYaku from '@/core/han-calculation/yakus/tsuuiisou-yaku'
3 | import Hand from '@/core/hand'
4 | import { Triplet, Pair } from '@/core/combination-classes'
5 | import { DotTile, BambooTile, DragonTile, WindTile, CharacterTile } from '@/core/tile-classes'
6 |
7 | test('tsuuiisou (all honors) valid hand with only honor tiles', () => {
8 | const hand = new Hand({
9 | concealedCombinations: [
10 | new Pair(new WindTile('east')),
11 | new Triplet(new WindTile('south')),
12 | new Triplet(new WindTile('north')),
13 | new Triplet(new DragonTile('green'))
14 | ],
15 | openCombinations: [
16 | new Triplet(new DragonTile('red'))
17 | ]
18 | })
19 |
20 | expect(new TsuuiisouYaku().check(hand)).toStrictEqual({ key: 'tsuuiisou', hanValue: 0, yakumanValue: 1 })
21 | })
22 |
23 | test('tsuuiisou (all honors) invalid hand because it contains dot tiles', () => {
24 | const hand = new Hand({
25 | concealedCombinations: [
26 | new Pair(new DotTile(1)),
27 | new Triplet(new WindTile('south')),
28 | new Triplet(new WindTile('north')),
29 | new Triplet(new DragonTile('green'))
30 | ],
31 | openCombinations: [
32 | new Triplet(new DragonTile('red'))
33 | ]
34 | })
35 |
36 | expect(new TsuuiisouYaku().check(hand)).toBeUndefined()
37 | })
38 |
39 | test('tsuuiisou (all honors) invalid hand because it contains bamboo tiles', () => {
40 | const hand = new Hand({
41 | concealedCombinations: [
42 | new Pair(new BambooTile(2)),
43 | new Triplet(new WindTile('south')),
44 | new Triplet(new WindTile('north')),
45 | new Triplet(new DragonTile('green'))
46 | ],
47 | openCombinations: [
48 | new Triplet(new DragonTile('red'))
49 | ]
50 | })
51 |
52 | expect(new TsuuiisouYaku().check(hand)).toBeUndefined()
53 | })
54 |
55 | test('tsuuiisou (all honors) invalid hand because it contains character tiles', () => {
56 | const hand = new Hand({
57 | concealedCombinations: [
58 | new Pair(new CharacterTile(3)),
59 | new Triplet(new WindTile('south')),
60 | new Triplet(new WindTile('north')),
61 | new Triplet(new DragonTile('green'))
62 | ],
63 | openCombinations: [
64 | new Triplet(new DragonTile('red'))
65 | ]
66 | })
67 |
68 | expect(new TsuuiisouYaku().check(hand)).toBeUndefined()
69 | })
70 |
--------------------------------------------------------------------------------
/tests/unit/hand.spec.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 | import { Pair, Triplet, Quad } from '@/core/combination-classes'
3 | import { DotTile } from '@/core/tile-classes'
4 | import Hand from '@/core/hand'
5 |
6 | test('create a hand with all default values', () => {
7 | const hand = new Hand()
8 |
9 | expect(hand).toMatchObject({
10 | concealedCombinations: [],
11 | openCombinations: [],
12 | roundWind: 'east',
13 | seatWind: 'east',
14 | winningType: 'tsumo',
15 | winningCombinationIndex: null,
16 | winningTileIndex: null,
17 | yakus: [],
18 | nbDora: 0
19 | })
20 |
21 | // getters don't work in toMatchObject
22 |
23 | expect(hand.isOpen).toBe(false)
24 | expect(hand.combinations).toStrictEqual([])
25 | expect(hand.winningCombination).toBeNull()
26 | expect(hand.winningTile).toBeNull()
27 | })
28 |
29 | test('create a hand with options', () => {
30 | const hand = new Hand({
31 | concealedCombinations: [
32 | new Pair(new DotTile(1)),
33 | new Triplet(new DotTile(2))
34 | ],
35 | openCombinations: [
36 | new Triplet(new DotTile(3)),
37 | new Triplet(new DotTile(4)),
38 | new Quad(new DotTile(5))
39 | ],
40 | roundWind: 'south',
41 | seatWind: 'west',
42 | winningCombinationIndex: 0,
43 | winningTileIndex: 0,
44 | winningType: 'ron',
45 | nbDora: 1,
46 | yakus: ['rinshan kaihou']
47 | })
48 |
49 | expect(hand).toMatchObject({
50 | concealedCombinations: [
51 | new Pair(new DotTile(1)),
52 | new Triplet(new DotTile(2))
53 | ],
54 | openCombinations: [
55 | new Triplet(new DotTile(3)),
56 | new Triplet(new DotTile(4)),
57 | new Quad(new DotTile(5))
58 | ],
59 | roundWind: 'south',
60 | seatWind: 'west',
61 | winningType: 'ron',
62 | winningCombinationIndex: 0,
63 | winningTileIndex: 0,
64 | yakus: ['rinshan kaihou'],
65 | nbDora: 1
66 | })
67 |
68 | // getters don't work in toMatchObject
69 |
70 | expect(hand.isOpen).toBe(true)
71 | expect(hand.combinations).toStrictEqual([
72 | new Pair(new DotTile(1)),
73 | new Triplet(new DotTile(2)),
74 | new Triplet(new DotTile(3)),
75 | new Triplet(new DotTile(4)),
76 | new Quad(new DotTile(5))
77 | ])
78 | expect(hand.winningCombination).toStrictEqual(new Pair(new DotTile(1)))
79 | expect(hand.winningTile).toStrictEqual(new DotTile(1))
80 | })
81 |
--------------------------------------------------------------------------------
/tests/unit/waits/is-kanchan-wait.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import isKanchanWait from '@/core/waits/is-kanchan-wait'
3 | import Hand from '@/core/hand'
4 | import { Pair, Triplet, Sequence } from '@/core/combination-classes'
5 | import { DotTile } from '@/core/tile-classes'
6 |
7 | describe('given the hand was won by completing a pair', () => {
8 | test('should be considered kanchan wait', () => {
9 | const hand = new Hand({
10 | concealedCombinations: [
11 | new Pair(new DotTile(1)),
12 | new Triplet(new DotTile(2)),
13 | new Triplet(new DotTile(3)),
14 | new Triplet(new DotTile(4)),
15 | new Triplet(new DotTile(5))
16 | ],
17 | winningCombinationIndex: 0,
18 | winningTileIndex: 0
19 | })
20 |
21 | expect(isKanchanWait(hand)).toBe(false)
22 | })
23 | })
24 |
25 | describe('given the hand was won by completing a triplet', () => {
26 | test('should not be considered kanchan wait', () => {
27 | const hand = new Hand({
28 | concealedCombinations: [
29 | new Pair(new DotTile(1)),
30 | new Triplet(new DotTile(2)),
31 | new Triplet(new DotTile(3)),
32 | new Triplet(new DotTile(4)),
33 | new Triplet(new DotTile(5))
34 | ],
35 | winningCombinationIndex: 1,
36 | winningTileIndex: 0
37 | })
38 |
39 | expect(isKanchanWait(hand)).toBe(false)
40 | })
41 | })
42 |
43 | describe('given the hand was won by completing a sequence', () => {
44 | test('should be considered kanchan wait if the winning tile was the tile in the middle of the combination', () => {
45 | const hand = new Hand({
46 | concealedCombinations: [
47 | new Pair(new DotTile(1)),
48 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
49 | new Triplet(new DotTile(3)),
50 | new Triplet(new DotTile(4)),
51 | new Triplet(new DotTile(5))
52 | ],
53 | winningCombinationIndex: 1,
54 | winningTileIndex: 1
55 | })
56 |
57 | expect(isKanchanWait(hand)).toBe(true)
58 | })
59 |
60 | test('should not be considered kanchan wait if the winning tile was not the tile in the middle of the combination', () => {
61 | const hand = new Hand({
62 | concealedCombinations: [
63 | new Pair(new DotTile(1)),
64 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
65 | new Triplet(new DotTile(3)),
66 | new Triplet(new DotTile(4)),
67 | new Triplet(new DotTile(5))
68 | ],
69 | winningCombinationIndex: 1,
70 | winningTileIndex: 0
71 | })
72 |
73 | expect(isKanchanWait(hand)).toBe(false)
74 | })
75 | })
76 |
--------------------------------------------------------------------------------
/tests/unit/waits/is-penchan-wait.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import isPenchanWait from '@/core/waits/is-penchan-wait'
3 | import Hand from '@/core/hand'
4 | import { Pair, Triplet, Sequence } from '@/core/combination-classes'
5 | import { DotTile } from '@/core/tile-classes'
6 |
7 | describe('given the hand was won by completing a pair', () => {
8 | test('should be considered penchan wait', () => {
9 | const hand = new Hand({
10 | concealedCombinations: [
11 | new Pair(new DotTile(1)),
12 | new Triplet(new DotTile(2)),
13 | new Triplet(new DotTile(3)),
14 | new Triplet(new DotTile(4)),
15 | new Triplet(new DotTile(5))
16 | ],
17 | winningCombinationIndex: 0,
18 | winningTileIndex: 0
19 | })
20 |
21 | expect(isPenchanWait(hand)).toBe(false)
22 | })
23 | })
24 |
25 | describe('given the hand was won by completing a triplet', () => {
26 | test('should not be considered penchan wait', () => {
27 | const hand = new Hand({
28 | concealedCombinations: [
29 | new Pair(new DotTile(1)),
30 | new Triplet(new DotTile(2)),
31 | new Triplet(new DotTile(3)),
32 | new Triplet(new DotTile(4)),
33 | new Triplet(new DotTile(5))
34 | ],
35 | winningCombinationIndex: 1,
36 | winningTileIndex: 0
37 | })
38 |
39 | expect(isPenchanWait(hand)).toBe(false)
40 | })
41 | })
42 |
43 | describe('given the hand was won by completing a sequence', () => {
44 | test('should not be considered penchan wait if the winning tile was the tile in the middle of the combination', () => {
45 | const hand = new Hand({
46 | concealedCombinations: [
47 | new Pair(new DotTile(1)),
48 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
49 | new Triplet(new DotTile(3)),
50 | new Triplet(new DotTile(4)),
51 | new Triplet(new DotTile(5))
52 | ],
53 | winningCombinationIndex: 1,
54 | winningTileIndex: 1
55 | })
56 |
57 | expect(isPenchanWait(hand)).toBe(false)
58 | })
59 |
60 | test('should be considered penchan wait if the winning tile had the value 3 on a 1-2-3 sequence', () => {
61 | const hand = new Hand({
62 | concealedCombinations: [
63 | new Pair(new DotTile(1)),
64 | new Sequence(new DotTile(1), new DotTile(2), new DotTile(3)),
65 | new Triplet(new DotTile(3)),
66 | new Triplet(new DotTile(4)),
67 | new Triplet(new DotTile(5))
68 | ],
69 | winningCombinationIndex: 1,
70 | winningTileIndex: 2
71 | })
72 |
73 | expect(isPenchanWait(hand)).toBe(true)
74 | })
75 |
76 | test('should be considered penchan wait if the winning tile had the value 7 on a 7-8-9 sequence', () => {
77 | const hand = new Hand({
78 | concealedCombinations: [
79 | new Pair(new DotTile(1)),
80 | new Sequence(new DotTile(7), new DotTile(8), new DotTile(9)),
81 | new Triplet(new DotTile(3)),
82 | new Triplet(new DotTile(4)),
83 | new Triplet(new DotTile(5))
84 | ],
85 | winningCombinationIndex: 1,
86 | winningTileIndex: 0
87 | })
88 |
89 | expect(isPenchanWait(hand)).toBe(true)
90 | })
91 |
92 | test('should not be considered penchan wait if the winning tile was the first or last tile of the combination (not 1-2-3 and 7-8-9)', () => {
93 | const hand = new Hand({
94 | concealedCombinations: [
95 | new Pair(new DotTile(1)),
96 | new Sequence(new DotTile(5), new DotTile(6), new DotTile(7)),
97 | new Triplet(new DotTile(3)),
98 | new Triplet(new DotTile(4)),
99 | new Triplet(new DotTile(5))
100 | ],
101 | winningCombinationIndex: 1,
102 | winningTileIndex: 0
103 | })
104 |
105 | expect(isPenchanWait(hand)).toBe(false)
106 | })
107 | })
108 |
--------------------------------------------------------------------------------
/tests/unit/waits/is-tanki-wait.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest'
2 | import isTankiWait from '@/core/waits/is-tanki-wait'
3 | import Hand from '@/core/hand'
4 | import { Pair, Triplet, Sequence } from '@/core/combination-classes'
5 | import { DotTile } from '@/core/tile-classes'
6 |
7 | describe('given the hand was won by completing a pair', () => {
8 | test('should be considered tanki wait', () => {
9 | const hand = new Hand({
10 | concealedCombinations: [
11 | new Pair(new DotTile(1)),
12 | new Triplet(new DotTile(2)),
13 | new Triplet(new DotTile(3)),
14 | new Triplet(new DotTile(4)),
15 | new Triplet(new DotTile(5))
16 | ],
17 | winningCombinationIndex: 0,
18 | winningTileIndex: 0
19 | })
20 |
21 | expect(isTankiWait(hand)).toBe(true)
22 | })
23 | })
24 |
25 | describe('given the hand was won by completing a triplet', () => {
26 | test('should not be considered tanki wait', () => {
27 | const hand = new Hand({
28 | concealedCombinations: [
29 | new Pair(new DotTile(1)),
30 | new Triplet(new DotTile(2)),
31 | new Triplet(new DotTile(3)),
32 | new Triplet(new DotTile(4)),
33 | new Triplet(new DotTile(5))
34 | ],
35 | winningCombinationIndex: 1,
36 | winningTileIndex: 0
37 | })
38 |
39 | expect(isTankiWait(hand)).toBe(false)
40 | })
41 | })
42 |
43 | describe('given the hand was won by completing a sequence', () => {
44 | test('should not be considered tanki wait', () => {
45 | const hand = new Hand({
46 | concealedCombinations: [
47 | new Pair(new DotTile(1)),
48 | new Sequence(new DotTile(2), new DotTile(3), new DotTile(4)),
49 | new Triplet(new DotTile(3)),
50 | new Triplet(new DotTile(4)),
51 | new Triplet(new DotTile(5))
52 | ],
53 | winningCombinationIndex: 1,
54 | winningTileIndex: 0
55 | })
56 |
57 | expect(isTankiWait(hand)).toBe(false)
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url'
2 |
3 | import { defineConfig, loadEnv } from 'vite'
4 | import vue from '@vitejs/plugin-vue'
5 | import { createHtmlPlugin } from 'vite-plugin-html'
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig(({ mode }) => {
9 | const env = loadEnv(mode, './') // reuse vite's env parser to inject into our index.html
10 |
11 | return {
12 | base: env.VITE_BASE_URL,
13 |
14 | plugins: [
15 | vue(),
16 |
17 | createHtmlPlugin({
18 | inject: {
19 | data: env
20 | }
21 | })
22 | ],
23 |
24 | resolve: {
25 | alias: {
26 | '@': fileURLToPath(new URL('./src', import.meta.url))
27 | }
28 | }
29 | }
30 | })
31 |
--------------------------------------------------------------------------------