Files

276 lines
9.1 KiB
TypeScript
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.
/**
* AI testleri — üç zorluk seviyesi, geçerli hamle üretme,
* pişti/alma fırsatı değerlendirme, puanlı kart koruma, strateji farkı.
*/
import { describe, expect, it } from 'vitest';
import { makeCard } from '../src/engine/deck';
import { decideDeterministic, decideEasy, decideHard, decideNormal, strategyFor } from '../src/engine/ai';
import type { AiView } from '../src/engine/engine';
import { PistiEngine } from '../src/engine/engine';
import type { Card, Difficulty } from '../src/engine/types';
function seededRng(seed: number): () => number {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/** Test için yapay AI görünümü üretir */
function makeView(overrides: Partial<AiView> = {}): AiView {
const hand: Card[] = [
makeCard('kupa', 5),
makeCard('karo', 9),
makeCard('sinek', 12),
makeCard('maca', 3),
];
return {
self: 1,
hand,
table: [makeCard('karo', 7)],
top: makeCard('karo', 7),
deckLeft: 20,
difficulty: 'normal',
team: 'B',
seenCards: [],
opponentHandSizes: [4, 4, 4, 4],
totalCaptured: { A: 0, B: 0 },
turn: 1,
...overrides,
};
}
describe('AI temel davranış', () => {
it('kolay AI elinden geçerli bir kart oynar', () => {
const view = makeView();
const d = decideEasy(view);
expect(view.hand.some((c) => c.id === d.card.id)).toBe(true);
expect(d.reason.length).toBeGreaterThan(0);
});
it('normal AI elinden geçerli bir kart oynar', () => {
const view = makeView();
const d = decideNormal(view);
expect(view.hand.some((c) => c.id === d.card.id)).toBe(true);
});
it('zor AI elinden geçerli bir kart oynar', () => {
const view = makeView();
const d = decideHard(view);
expect(view.hand.some((c) => c.id === d.card.id)).toBe(true);
});
it('boş elle AI karar veremez (hata fırlatır)', () => {
const view = makeView({ hand: [] });
expect(() => decideEasy(view)).toThrow();
expect(() => decideNormal(view)).toThrow();
expect(() => decideHard(view)).toThrow();
});
});
describe('AI pişti farkındalığı', () => {
it('normal AI kesin pişti fırsatını kaçırmaz (yerde tek kart)', () => {
const top = makeCard('karo', 5);
const view = makeView({
hand: [makeCard('kupa', 5), makeCard('sinek', 2), makeCard('maca', 9)],
table: [top],
top,
});
const d = decideNormal(view);
expect(d.card.rank).toBe(5);
});
it('zor AI kesin pişti fırsatını kaçırmaz', () => {
const top = makeCard('sinek', 12);
const view = makeView({
hand: [makeCard('kupa', 12), makeCard('karo', 4), makeCard('maca', 8)],
table: [top],
top,
});
const d = decideHard(view);
expect(d.card.rank).toBe(12);
});
it('yerde tek kart ve aynı değer VARSA pişti tercih edilir (vale değil)', () => {
const top = makeCard('maca', 3);
const view = makeView({
hand: [makeCard('sinek', 3), makeCard('kupa', 11), makeCard('karo', 7)],
table: [top],
top,
});
const d = decideHard(view);
expect(d.card.rank).toBe(3);
expect(d.card.rank).not.toBe(11);
});
});
describe('AI kart alma farkındalığı', () => {
it('masada puan varken AI almaya çalışır', () => {
const top = makeCard('karo', 8);
const view = makeView({
// masada karo 10 (3 puan) + as (1 puan); üstteki karo 8
hand: [makeCard('sinek', 8), makeCard('kupa', 2), makeCard('maca', 4)],
table: [makeCard('karo', 10), makeCard('kupa', 1), top],
top,
});
const d = decideNormal(view);
expect(d.card.rank).toBe(8);
});
it('vale masada çok kart varken kullanılır', () => {
const top = makeCard('karo', 6);
const view = makeView({
hand: [makeCard('kupa', 11), makeCard('sinek', 2), makeCard('maca', 4)],
table: [makeCard('sinek', 3), makeCard('kupa', 4), makeCard('karo', 10), top],
top,
});
const d = decideHard(view);
expect(d.card.rank).toBe(11); // masa değerli (karo 10 var) → vale harcanır
});
});
describe('AI puanlı kart koruma', () => {
it('normal AI alma yoksa puanlı kartı (vale) boşa atmaz', () => {
const top = makeCard('kupa', 7);
const view = makeView({
hand: [makeCard('maca', 11), makeCard('sinek', 4), makeCard('karo', 6)],
table: [top],
top,
});
const d = decideNormal(view);
expect(d.card.rank).not.toBe(11);
});
it('normal AI karo 10 (3 puan) boşa atmaz', () => {
const top = makeCard('kupa', 2);
const view = makeView({
hand: [makeCard('karo', 10), makeCard('sinek', 4), makeCard('maca', 6)],
table: [top],
top,
});
const d = decideNormal(view);
expect(d.card.suit === 'karo' && d.card.rank === 10).toBe(false);
});
it('zor AI masayı tek kart bırakıp pişti riski yüksekse o rankı atmaz', () => {
// masada tek kart yok, ama elimizdeki 3 aynı ranktan 3'ü görülmüşse risk düşük
const seen: Card[] = [makeCard('kupa', 4), makeCard('karo', 4), makeCard('sinek', 4)];
const view = makeView({
hand: [makeCard('maca', 4), makeCard('kupa', 7)],
table: [makeCard('karo', 9)],
top: makeCard('karo', 9),
seenCards: seen,
});
const d = decideHard(view);
// 4 atmak güvenli (4'ün 4'ü de biliniyor/işlemde)
expect(d.card.id).toBe('maca-4');
});
});
describe('Zorluk seviyesi strateji seçimi', () => {
it('strategyFor doğru fonksiyonu döndürür', () => {
const kolay = strategyFor('kolay');
const normal = strategyFor('normal');
const zor = strategyFor('zor');
expect(kolay).toBe(decideEasy);
expect(normal).toBe(decideNormal);
expect(zor).toBe(decideHard);
});
it('bilinmeyen zorluk hata verir', () => {
expect(() => strategyFor('imkansiz' as Difficulty)).toThrow();
});
it('deterministik karar aynı seed ile aynı kartı verir', () => {
const view = makeView();
const a = decideDeterministic(view, 2);
const b = decideDeterministic(view, 2);
expect(a.card.id).toBe(b.card.id);
});
});
describe('AI ile tam oyun (entegrasyon)', () => {
it('3 AI ile oyun baştan sona oynanabilir (konsol hatası yok)', () => {
const engine = new PistiEngine({
soloVsAi: true,
difficulties: { 1: 'kolay', 2: 'normal', 3: 'zor' },
rng: seededRng(777),
});
engine.createGame();
engine.startGame();
let guard = 0;
while (!engine.getState().finished && guard < 400) {
const st = engine.getState();
const strategy = strategyFor(st.players[st.turn]?.difficulty ?? 'normal');
const decision = engine.decideAiMove(strategy);
if (!decision) break;
const res = engine.playCard(st.turn, decision.card);
expect(res.ok, `${guard}. hamle: ${res.error ?? ''}`).toBe(true);
guard++;
}
expect(engine.getState().finished).toBe(true);
const ex = engine.getExtras();
const total =
ex.captured[0].length + ex.captured[1].length + ex.captured[2].length + ex.captured[3].length;
expect(total).toBe(52);
});
it('zor AI, kolay AI\'a karşı uzun vadede daha iyi skor üretir', () => {
// 12 deterministik oyun oynat: zor AI takımı vs kolay AI takımı
let hardWins = 0;
let softWins = 0;
for (let seed = 1; seed <= 12; seed++) {
const engine = new PistiEngine({
soloVsAi: true,
// Takım A (0,2) zor, Takım B (1,3) kolay
difficulties: { 0: 'zor', 2: 'zor', 1: 'kolay', 3: 'kolay' },
rng: seededRng(seed * 31),
});
engine.createGame();
engine.startGame();
let guard = 0;
while (!engine.getState().finished && guard < 400) {
const st = engine.getState();
const strategy = strategyFor(st.players[st.turn]?.difficulty ?? 'normal');
const decision = engine.decideAiMove(strategy);
if (!decision) break;
engine.playCard(st.turn, decision.card);
guard++;
}
const t = engine.getExtras().totals;
if (t.A > t.B) hardWins++;
else if (t.B > t.A) softWins++;
}
// Zor AI'ın en azından kaybetmemesi beklenir (deterministik olmayan kararlar var,
// bu yüzden katı eşitlik değil, anlamlı bir fark arıyoruz)
expect(hardWins).toBeGreaterThanOrEqual(softWins);
});
it('AI hiçbir zaman elinde olmayan kartı seçmez', () => {
for (let seed = 1; seed <= 8; seed++) {
const engine = new PistiEngine({
soloVsAi: true,
difficulties: { 1: 'kolay', 2: 'normal', 3: 'zor' },
rng: seededRng(seed * 7),
});
engine.createGame();
engine.startGame();
let guard = 0;
while (!engine.getState().finished && guard < 400) {
const st = engine.getState();
const strategy = strategyFor(st.players[st.turn]?.difficulty ?? 'normal');
const decision = engine.decideAiMove(strategy);
if (!decision) break;
const inHand = st.players[st.turn]?.hand.some((c) => c.id === decision.card.id) ?? false;
expect(inHand, `seed ${seed}: AI elinde olmayan kart seçti`).toBe(true);
engine.playCard(st.turn, decision.card);
guard++;
}
}
});
});