Files

336 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
rules.js — Okey kuralları: okey/joker tanımı, per (set/run) bulma,
el geçerliliği (kazanma kontrolü), bot için atış önerisi.
UMD-uyumlu IIFE.
========================================================================== */
(function (root, factory) {
var mod = factory(
(typeof module === 'object' && module.exports)
? require('./tiles.js')
: root.OkeyTiles
);
if (typeof module === 'object' && module.exports) {
module.exports = mod;
} else {
root.OkeyRules = mod;
}
}(typeof self !== 'undefined' ? self : this, function (Tiles) {
'use strict';
var COLORS = ['kirmizi', 'siyah', 'mavi', 'sari'];
/* ----------------------------- JOKER TANIMLARI ---------------------- */
function isOkey(tile, indicator) {
return Tiles.isRealOkey(tile, indicator);
}
function isFakeOkey(tile) {
return !!(tile && tile.isFakeOkey);
}
/** Okey VEYA sahte okey => joker (per içinde her taşın yerine geçebilir) */
function isJoker(tile, indicator) {
return isOkey(tile, indicator) || isFakeOkey(tile);
}
function countJokers(tiles, indicator) {
var okey = 0, fake = 0;
for (var i = 0; i < tiles.length; i++) {
if (isFakeOkey(tiles[i])) fake++;
else if (isOkey(tiles[i], indicator)) okey++;
}
return { okey: okey, fake: fake, total: okey + fake };
}
/** Normal (joker olmayan, geçerli renkli) taşlar */
function normalTiles(tiles, indicator) {
var out = [];
for (var i = 0; i < tiles.length; i++) {
if (!isJoker(tiles[i], indicator)) out.push(tiles[i]);
}
return out;
}
/* ------------------------------- PER'LER ---------------------------- */
/**
* Per (grup) modelleri:
* SET : aynı sayı, FARKLI renklerden >= 3 taş (örn. 5kirmizi 5siyah 5mavi)
* RUN : aynı renk, ardışık sayılar >= 3 taş (örn. kirmizi 4,5,6)
* Joker (okey / sahte okey) herhangi bir taşın YERİNE geçebilir.
*/
/** Bir per'in geçerli olup olmadığı: normalize edilmiş (rank/color) listesi */
function isValidSet(entries) {
if (entries.length < 3) return false;
var ranks = {}, colors = {};
for (var i = 0; i < entries.length; i++) {
ranks[entries[i].rank] = true;
colors[entries[i].color] = true;
}
var rk = Object.keys(ranks);
var cl = Object.keys(colors);
return rk.length === 1 && cl.length === entries.length && cl.length <= 4;
}
function isValidRun(entries) {
if (entries.length < 3) return false;
var color = entries[0].color;
var rs = [];
for (var i = 0; i < entries.length; i++) {
if (entries[i].color !== color) return false;
rs.push(entries[i].rank);
}
rs.sort(function (a, b) { return a - b; });
for (var k = 1; k < rs.length; k++) {
if (rs[k] !== rs[k - 1] + 1) return false;
}
return true;
}
function isValidPer(entries) {
return isValidSet(entries) || isValidRun(entries);
}
/* --------------- Partition (el -> perlere ayrılabilir mi?) ----------- */
/**
* Taş yardımcı anahtarı: "renk:rank"
*/
function key(t) { return t.color + ':' + t.rank; }
/**
* Bir multiset'ten (string anahtar -> adet) geçerli per çıkarma denemesi
* için aday üretimi. Backtracking ile tam bölümleme arar.
*/
/**
* Elimizdeki normal taşları (jokerler hariç) set/run perlerine ayırıp
* kalan taş sayısını ve kalanları döndürür. Jokerler her adımda boşluk
* doldurmak için kullanılır.
*
* @param {Array} normals joker olmayan taşlar
* @param {number} jokerCount kullanılabilir joker sayısı
* @param {Object} memo (opsiyonel) memoization
*/
function canPartition(normals, jokerCount, memo) {
memo = memo || {};
if (normals.length === 0) return { ok: true, usedJokers: 0 };
var sig = normals.slice().map(key).sort().join('|') + '#' + jokerCount;
if (memo[sig] !== undefined) return memo[sig];
// Hiç taş kalmadıysa başarılı
if (normals.length === 0) {
memo[sig] = { ok: true, usedJokers: 0 };
return memo[sig];
}
var remain = normals.slice();
// 1) DOĞAL SET dene: aynı rank, farklı renkler >= 3
var byRank = {};
for (var i = 0; i < remain.length; i++) {
var r = remain[i].rank;
if (!byRank[r]) byRank[r] = [];
byRank[r].push(remain[i]);
}
var rks = Object.keys(byRank);
for (var a = 0; a < rks.length; a++) {
var group = byRank[rks[a]];
// benzersiz renkleri al (aynı renk+rank 2 kopya varsa birini seç)
var seenColor = {};
var uniq = [];
for (var g = 0; g < group.length; g++) {
if (!seenColor[group[g].color]) {
seenColor[group[g].color] = true;
uniq.push(group[g]);
}
}
if (uniq.length >= 3) {
for (var take = 3; take <= uniq.length; take++) {
var subset = uniq.slice(0, take);
var rest = removeTiles(remain, subset);
var res = canPartition(rest, jokerCount, memo);
if (res.ok) {
memo[sig] = { ok: true, usedJokers: res.usedJokers };
return memo[sig];
}
}
// joker ile 3'e tamamlanabilir mi? (2 farklı renk + joker)
if (uniq.length === 2 && jokerCount >= 1) {
var rest2 = removeTiles(remain, uniq.slice(0, 2));
var res2 = canPartition(rest2, jokerCount - 1, memo);
if (res2.ok) {
memo[sig] = { ok: true, usedJokers: res2.usedJokers + 1 };
return memo[sig];
}
}
}
}
// 2) DOĞAL RUN dene: aynı renk, ardışık >= 3
var byColor = {};
for (var m = 0; m < remain.length; m++) {
var col = remain[m].color;
if (!byColor[col]) byColor[col] = {};
byColor[col][remain[m].rank] = remain[m];
}
var cols = Object.keys(byColor);
for (var c = 0; c < cols.length; c++) {
var colorMap = byColor[cols[c]];
var ranks = Object.keys(colorMap).map(Number).sort(function (x, y) { return x - y; });
// ardışık koşuları bul (uzunluk >= 3)
var runStart = null, prev = null, runSet = [];
for (var ri = 0; ri <= ranks.length; ri++) {
var cur = ranks[ri];
if (prev !== null && cur === prev + 1) {
runSet.push(cur);
} else {
if (runSet.length >= 3) {
var tilesToRemove = [];
for (var q = 0; q < runSet.length; q++) tilesToRemove.push(colorMap[runSet[q]]);
var restR = removeTiles(remain, tilesToRemove);
var resR = canPartition(restR, jokerCount, memo);
if (resR.ok) {
memo[sig] = { ok: true, usedJokers: resR.usedJokers };
return memo[sig];
}
}
runSet = [];
}
if (cur !== undefined) runSet.push(cur);
prev = (cur === undefined) ? prev : cur;
}
}
// 3) JOKER ile RUN tamamlama: aynı renkte 2 ardışık + joker
if (jokerCount >= 1) {
for (var c2 = 0; c2 < cols.length; c2++) {
var cm = byColor[cols[c2]];
var rk2 = Object.keys(cm).map(Number);
for (var i2 = 0; i2 < rk2.length; i2++) {
for (var j2 = i2 + 1; j2 < rk2.length; j2++) {
if (Math.abs(rk2[i2] - rk2[j2]) === 1) {
// iki taş + 1 joker = geçerli run
var pair = [cm[rk2[i2]], cm[rk2[j2]]];
var restJ = removeTiles(remain, pair);
var resJ = canPartition(restJ, jokerCount - 1, memo);
if (resJ.ok) {
memo[sig] = { ok: true, usedJokers: resJ.usedJokers + 1 };
return memo[sig];
}
}
}
}
}
// 3) JOKER ile SET tamamlama: aynı rank 2 farklı renk + joker
if (jokerCount >= 1) {
for (var a3 = 0; a3 < rks.length; a3++) {
var grp3 = byRank[rks[a3]];
var seenC3 = {}, uniq3 = [];
for (var g3 = 0; g3 < grp3.length; g3++) {
if (!seenC3[grp3[g3].color]) { seenC3[grp3[g3].color] = true; uniq3.push(grp3[g3]); }
}
if (uniq3.length === 2) {
var restS3 = removeTiles(remain, uniq3.slice(0, 2));
var resS3 = canPartition(restS3, jokerCount - 1, memo);
if (resS3.ok) {
memo[sig] = { ok: true, usedJokers: resS3.usedJokers + 1 };
return memo[sig];
}
}
}
}
}
memo[sig] = { ok: false, usedJokers: 0 };
return memo[sig];
}
/** `list` içinden `tiles` öğelerini (id ile) çıkarılmış kopya döndür */
function removeTiles(list, tiles) {
var ids = {};
for (var i = 0; i < tiles.length; i++) ids[tiles[i].id] = true;
var out = [];
for (var j = 0; j < list.length; j++) {
if (!ids[list[j].id]) out.push(list[j]);
}
return out;
}
/**
* El kazanabilir mi? Klasik: 14 taş, tümü set/run perlerine ayrılmalı.
* (Jokerler serbestçe boşluk doldurur.)
*/
function isWinningHand(hand, indicator) {
if (!hand || hand.length !== 14) return false;
var normals = normalTiles(hand, indicator);
var jk = countJokers(hand, indicator);
// Normal taşların toplamı + joker <= 14
var res = canPartition(normals, jk.total, {});
return res.ok === true;
}
/** Kaç adet geçerli per'e ayrılabiliyor — bot/öneri için skor */
function partitionScore(hand, indicator) {
var normals = normalTiles(hand, indicator);
var jk = countJokers(hand, indicator);
var res = canPartition(normals, jk.total, {});
return res.ok ? 999 : (normals.length - res.usedJokers);
}
/**
* Bot için: elden hangi taş en uygun şekilde ATILIR?
* Basit skor: taşın "yardımcılığı". Aynı rank'tan farklı renk çoksa / run
* komşusu varsa değerli. Hiç yardımcısı olmayan en yalnız taş atılır.
* Jokerler asla atılmaz.
*/
function bestDiscardSuggestion(hand, indicator) {
var normals = normalTiles(hand, indicator);
if (normals.length === 0) return hand[0];
var best = null, bestScore = Infinity;
for (var i = 0; i < normals.length; i++) {
var t = normals[i];
var score = 0;
var sameRankDifferentColor = 0;
var runNeighbors = 0;
for (var j = 0; j < normals.length; j++) {
if (i === j) continue;
var o = normals[j];
if (o.rank === t.rank && o.color !== t.color) sameRankDifferentColor++;
if (o.color === t.color && Math.abs(o.rank - t.rank) === 1) runNeighbors++;
}
score = sameRankDifferentColor * 2 + runNeighbors * 2;
// aynı rank+renkten ikinci kopya varsa at (weak)
for (var k = 0; k < normals.length; k++) {
if (k !== i && normals[k].rank === t.rank && normals[k].color === t.color) score -= 1;
}
if (score < bestScore) {
bestScore = score;
best = t;
}
}
return best || normals[0];
}
return {
COLORS: COLORS,
isOkey: isOkey,
isFakeOkey: isFakeOkey,
isJoker: isJoker,
countJokers: countJokers,
normalTiles: normalTiles,
isValidSet: isValidSet,
isValidRun: isValidRun,
isValidPer: isValidPer,
canPartition: canPartition,
removeTiles: removeTiles,
isWinningHand: isWinningHand,
partitionScore: partitionScore,
bestDiscardSuggestion: bestDiscardSuggestion
};
}));