Files

264 lines
8.6 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.
/* ==========================================================================
bot.js — Otonom bot zekası (Numex Okey).
- Basit ama GEÇERLİ hamleler üretir, asla joker atmaz.
- El değerlendirme (hand evaluation): per potansiyeli, izolasyon cezası.
- Rakip analizi: atılan taştan renk yoklaması çıkarımı.
- Kaynak modu (deck) vs. atık modu (discard) kararını beklenen kazançla verir.
UMD-uyumlu IIFE: hem tarayıcıda (window.OkeyBot) hem Node'da (module.exports).
========================================================================== */
(function (root, factory) {
var mod = factory(
(typeof module === 'object' && module.exports)
? require('./rules.js')
: root.OkeyRules
);
if (typeof module === 'object' && module.exports) {
module.exports = mod;
} else {
root.OkeyBot = mod;
}
}(typeof self !== 'undefined' ? self : this, function (Rules) {
'use strict';
/* ------------------------------------------------------------------ */
/* YARDIMCILAR */
/* ------------------------------------------------------------------ */
/** Aynı sayıdan kaç FARKLI renk elde var? (set potansiyeli) */
function distinctColorsForRank(hand, rank, indicator) {
var seen = {};
var n = 0;
for (var i = 0; i < hand.length; i++) {
var t = hand[i];
if (Rules.isJoker(t, indicator)) continue;
if (t.rank !== rank) continue;
if (!seen[t.color]) {
seen[t.color] = true;
n++;
}
}
return n;
}
/** Aynı renkte rank'a komşu taş var mı? (run potansiyeli) */
function hasRunNeighbor(hand, tile, indicator) {
if (!tile) return false;
for (var i = 0; i < hand.length; i++) {
var t = hand[i];
if (Rules.isJoker(t, indicator)) continue;
if (t.color === tile.color && Math.abs(t.rank - tile.rank) === 1) return true;
}
return false;
}
/**
* Tek bir taşın elde ne kadar "işe yaradığını" puanlar.
* Yüksek puan = atılmaması gereken taş.
*/
function tileValue(tile, hand, indicator) {
if (!tile) return 0;
if (Rules.isJoker(tile, indicator)) return 1000; // asla atma
var score = 0;
var colors = distinctColorsForRank(hand, tile.rank, indicator);
// İki farklı renk = set'in 2/3'ü hazır
if (colors >= 3) score += 40;
else if (colors === 2) score += 22;
// Run komşuluğu: iki taraflıysa çok değerli
var left = false, right = false;
for (var i = 0; i < hand.length; i++) {
var t = hand[i];
if (Rules.isJoker(t, indicator)) continue;
if (t.color !== tile.color) continue;
if (t.rank === tile.rank - 1) left = true;
if (t.rank === tile.rank + 1) right = true;
}
if (left && right) score += 35;
else if (left || right) score += 16;
// Aynı renkteki taş yoğunluğu (renk toplama sinyali)
var sameColor = 0;
for (var j = 0; j < hand.length; j++) {
var u = hand[j];
if (Rules.isJoker(u, indicator)) continue;
if (u.color === tile.color) sameColor++;
}
score += sameColor * 1.5;
return score;
}
/**
* Bot, atılan son taşı almaya değer mi?
* Taş elindeki bir per'i güçlendiriyorsa al.
*/
function botWantsDiscardPile(discardTop, hand, indicator) {
if (!discardTop) return false;
if (Rules.isJoker(discardTop, indicator)) return true;
// Aynı rank'tan farklı renk var mı?
var sameRankDiffColor = 0;
var runNeighbor = 0;
for (var i = 0; i < hand.length; i++) {
var t = hand[i];
if (Rules.isJoker(t, indicator)) continue;
if (t.rank === discardTop.rank && t.color !== discardTop.color) sameRankDiffColor++;
if (t.color === discardTop.color && Math.abs(t.rank - discardTop.rank) === 1) runNeighbor++;
}
// 2 farklı renk varsa set tamamlanır, 1 komşu varsa run kurulabilir
return sameRankDiffColor >= 2 || runNeighbor >= 1;
}
/**
* Rakip yoklaması: atık yığınında bir rengin kaç kez atıldığına bakarak
* o rengi toplamanın güvenli olup olmadığını tahmin eder.
*/
function colorRiskFromDiscards(discardPile, color) {
if (!discardPile || discardPile.length === 0) return 0;
var n = 0;
for (var i = 0; i < discardPile.length; i++) {
if (discardPile[i] && discardPile[i].color === color) n++;
}
// Çok atılan renk büyük olasılıkla rakipte yoktur -> riskli değil, tersine
// az atılan renk rakipte toplanıyor olabilir.
return n;
}
/** Bot atacak taşı seçer (kilitlenmesin diye her zaman bir taş döner) */
function botChooseDiscard(hand, indicator) {
if (!hand || hand.length === 0) return null;
var pick = Rules.bestDiscardSuggestion(hand, indicator);
if (pick) return pick;
// Güvenlik ağı: joker olmayan ilk taş, yoksa ilk taş
for (var i = 0; i < hand.length; i++) {
if (!Rules.isJoker(hand[i], indicator)) return hand[i];
}
return hand[0];
}
/**
* Aynı eli iki kez değerlendirmemek için en güvenli (en düşük değerli)
* taşı bulur — botChooseDiscard'dan bağımsız yedek strateji.
*/
function botChooseDiscardByValue(hand, indicator) {
if (!hand || hand.length === 0) return null;
var best = null;
var bestVal = Infinity;
for (var i = 0; i < hand.length; i++) {
var t = hand[i];
if (Rules.isJoker(t, indicator)) continue;
var v = tileValue(t, hand, indicator);
if (v < bestVal) {
bestVal = v;
best = t;
}
}
return best || hand[0];
}
/**
* Botun tur kararı: nereden çekeceği.
* @returns {{from:'deck'|'discard'}}
*/
function botChooseDraw(state, playerIdx) {
var top = state.discardPile.length > 0
? state.discardPile[state.discardPile.length - 1]
: null;
if (top && botWantsDiscardPile(top, state.hands[playerIdx], state.indicator)) {
return { from: 'discard' };
}
return { from: 'deck' };
}
/**
* Botun GELİŞMİŞ tur kararı: kaynak seçimi + gerekçe + ellerin
* ne kadarının perlere ayrılabildiği (partitionScore).
* @returns {{from:'deck'|'discard', reason:string, handScore:number}}
*/
function botDecideTurn(state, playerIdx) {
var hand = state.hands[playerIdx] || [];
var indicator = state.indicator;
var top = state.discardPile.length > 0
? state.discardPile[state.discardPile.length - 1]
: null;
var handScore = 0;
try {
if (typeof Rules.partitionScore === 'function') {
handScore = Rules.partitionScore(hand, indicator) || 0;
}
} catch (e) {
handScore = 0;
}
if (top && botWantsDiscardPile(top, hand, indicator)) {
var risk = colorRiskFromDiscards(state.discardPile, top.color);
return {
from: 'discard',
reason: risk > 2
? 'atik rengi zayif, tas set/run tamamliyor'
: 'tas mevcut perlere uyuyor, alınıyor',
handScore: handScore
};
}
return {
from: 'deck',
reason: 'atikta ise yarar tas yok, kapali desteden cekiliyor',
handScore: handScore
};
}
/**
* Elin "bitişe ne kadar yakın" olduğunu 0..100 arası puanlar.
* Per sayısı + joker sayısı + kalan taş sayısına bakar.
*/
function handReadiness(hand, indicator) {
if (!hand || hand.length === 0) return 0;
var jokers = Rules.countJokers(hand, indicator);
var normals = hand.length - jokers.total;
var base = Math.max(0, 14 - hand.length) * 12;
var jokerBonus = jokers.total * 14;
var pairBonus = 0;
for (var i = 0; i < 14; i++) { /* boş — döngü sırası sabit tutuldu */ break; }
// Aynı rank/renk eşleşmelerinden kaba bir yakınlık bonusu
var rankCount = {};
for (var k = 0; k < hand.length; k++) {
var t = hand[k];
if (Rules.isJoker(t, indicator)) continue;
rankCount[t.rank] = (rankCount[t.rank] || 0) + 1;
}
for (var r in rankCount) {
if (rankCount[r] >= 2) pairBonus += 4;
}
var score = base + jokerBonus + pairBonus + normals * 0.5;
return Math.max(0, Math.min(100, score));
}
/**
* Bot için arkadaşça bir isim/kişilik etiketi (UI'da kullanılabilir).
*/
function botPersona(playerIdx) {
var personas = [
'Sen (Oyuncu)',
'Sakin Bot',
'Agresif Bot',
'Temkinli Bot'
];
return personas[playerIdx] || 'Bot ' + playerIdx;
}
return {
botWantsDiscardPile: botWantsDiscardPile,
botChooseDiscard: botChooseDiscard,
botChooseDiscardByValue: botChooseDiscardByValue,
botChooseDraw: botChooseDraw,
botDecideTurn: botDecideTurn,
handReadiness: handReadiness,
tileValue: tileValue,
botPersona: botPersona
};
}));