feat: CI creates Gitea releases with changelog, app polls for updates on startup
Some checks failed
Android Build & Publish / android (push) Failing after 2m0s
Some checks failed
Android Build & Publish / android (push) Failing after 2m0s
- android-build.yml: fetch full history+tags, embed VITE_APP_BUILD, add step to create a tagged Gitea release (build-N) with markdown changelog and APK release assets after every push; bump permissions to contents:write - src/game/update-check.ts: polls Gitea releases/latest, compares build-N tag against CURRENT_BUILD (0 in dev), returns UpdateInfo or null; dismissal persisted to localStorage - src/vite-env.d.ts: TypeScript env declarations for VITE_APP_BUILD - src/scenes/MenuScene.ts: fire-and-forget update check on menu load; renders dismissible bottom-bar banner with optional APK download link - src/game/ai.ts: early-game empty-table dump heuristic (safest card first)
This commit is contained in:
@@ -239,6 +239,51 @@ function checkForcedMove(legalMoves: AIMove[], table: Card[]): AIMove | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Danger score for dumping a card on an empty table.
|
||||
* Lower = safer: fewer capture-combo possibilities + less strategic value.
|
||||
*
|
||||
* Primary combo-capture proxy: face value (higher → more subsets of future
|
||||
* table cards can sum to it, letting opponents capture it indirectly).
|
||||
* Category penalties: settebello, non-coin 7 (primiera), other denara, high
|
||||
* primiera-value cards (1/5/6 whose primiera contribution ≥ 15 pts).
|
||||
*/
|
||||
function emptyTableDumpDanger(card: Card): number {
|
||||
let danger = card.value; // 1-10: direct proxy for combo-capture breadth
|
||||
if (card.id === 'denara_7') danger += 50; // settebello — never dump
|
||||
else if (card.value === 7) danger += 20; // non-coin 7: top primiera
|
||||
else if (card.suit === 'denara') danger += 15; // coin card
|
||||
// High-primiera cards (1=16, 5=15, 6=18) get a small extra penalty.
|
||||
if (card.value !== 7 && (PRIMIERA_VALUES[card.value] ?? 0) >= 15) danger += 5;
|
||||
return danger;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the table is empty, choose the least-dangerous dump move.
|
||||
*
|
||||
* Rank 1 (primary): highest count of same face-value in our hand
|
||||
* — opponent holds fewer of that value → harder to
|
||||
* capture it with a direct same-value play.
|
||||
* Rank 2 (secondary): lowest emptyTableDumpDanger() score
|
||||
* — lower face value (fewer capture combos) and
|
||||
* not a strategically valuable card (7, denara, etc.).
|
||||
*/
|
||||
function pickSafestEmptyTableDump(hand: Card[], legalMoves: AIMove[]): AIMove | null {
|
||||
const dumps = legalMoves.filter(m => m.capture.length === 0);
|
||||
if (dumps.length === 0) return null;
|
||||
|
||||
const countByValue = new Map<number, number>();
|
||||
for (const c of hand) countByValue.set(c.value, (countByValue.get(c.value) ?? 0) + 1);
|
||||
|
||||
const sorted = [...dumps].sort((a, b) => {
|
||||
const cntA = countByValue.get(a.card.value) ?? 1;
|
||||
const cntB = countByValue.get(b.card.value) ?? 1;
|
||||
if (cntB !== cntA) return cntB - cntA; // more in hand → safer (desc)
|
||||
return emptyTableDumpDanger(a.card) - emptyTableDumpDanger(b.card); // less danger (asc)
|
||||
});
|
||||
return sorted[0];
|
||||
}
|
||||
|
||||
function buildFallbackInference(tracker: CardTracker | undefined): CardInferenceEngine {
|
||||
return new CardInferenceEngine(tracker ?? new CardTracker());
|
||||
}
|
||||
@@ -315,6 +360,12 @@ function advancedMove(
|
||||
const forced = checkForcedMove(legalMoves, state.table);
|
||||
if (forced) return forced;
|
||||
|
||||
// Early-game: empty table → no captures possible, dump the safest card.
|
||||
if (state.table.length === 0) {
|
||||
const safe = pickSafestEmptyTableDump(state.players[playerIdx].hand, legalMoves);
|
||||
if (safe) return safe;
|
||||
}
|
||||
|
||||
const myTeam = teamOf(playerIdx);
|
||||
const categoryStates = getCategoryStates(state, myTeam);
|
||||
const phase = getPhase(state);
|
||||
@@ -486,6 +537,23 @@ async function masterMove(
|
||||
}
|
||||
}
|
||||
|
||||
// Early-game: empty table → no captures possible, dump the safest card.
|
||||
if (state.table.length === 0) {
|
||||
const safe = pickSafestEmptyTableDump(state.players[playerIdx].hand, legalMoves);
|
||||
if (safe) {
|
||||
reportDecisionProgress(onProgress, 'master', startedAt, timing, profile.timeBudgetMs, 1, 1, {
|
||||
cardsRemaining,
|
||||
sampleCount: 0,
|
||||
maxDepth: 0,
|
||||
completedDepth: 0,
|
||||
rootMoveCount: legalMoves.length,
|
||||
timedOut: false,
|
||||
aspirationExpansions: 0,
|
||||
});
|
||||
return safe;
|
||||
}
|
||||
}
|
||||
|
||||
const myTeam = teamOf(playerIdx);
|
||||
const categoryStates = getCategoryStates(state, myTeam);
|
||||
const parityState = analyzeTableParity(state.table);
|
||||
|
||||
Reference in New Issue
Block a user