refine wordle AI balance, add Catan tutorial, fix opponent select layout, enable word category
- Adjust Wordle AI skill profiles to lower win rates and increase blunder chances for better difficulty curve - Center Wordle player portrait vertically with the playfield - Add Steve's humorous tutorial for Settlers of Catan (markdown + assets) - Update OpponentSelectScene to hide playfield options for word games and adjust scroll height - Fix migration script to temporarily disable foreign keys to allow schema changes - Add migration to extend games.category to include 'word'
This commit is contained in:
parent
690d05b28d
commit
17133787c1
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,201 @@
|
|||
# Settlers of Catan — A Guide by Steve
|
||||
|
||||
*Greetings, carbon-based lifeforms. I am Steve. I have traversed 14.2 light-years through the cold void of space to arrive on your planet, and my ship's universal translator tells me this game is called "Settlers of Catan." In my galaxy, we settled entire star systems before breakfast. But here we are. Let me explain your… quaint little board game.*
|
||||
|
||||
---
|
||||
|
||||
## What Is This Game?
|
||||
|
||||
Settlers of Catan is a multi-player strategy game (3–4 players) in which you build settlements, cities, and roads on a modular board made of hexagonal terrain tiles. The goal is to accumulate **10 victory points** — the first to reach that number wins. It is not a difficult concept; your species manages it just fine.
|
||||
|
||||
**Winning requires:**
|
||||
- Each **settlement** = 1 point
|
||||
- Each **city** = 2 points
|
||||
- **Longest Road** achievement = 2 points
|
||||
- **Largest Army** achievement = 2 points
|
||||
- Hidden **Victory Point** development cards = 1 point each
|
||||
|
||||
Build wisely. Or don't. I have seen civilizations collapse for less.
|
||||
|
||||
---
|
||||
|
||||
## The Board
|
||||
|
||||
The board is constructed from **19 hexagonal tiles** representing five terrain types:
|
||||
|
||||
| Terrain | Resource | Frequency |
|
||||
|---------|----------|-----------|
|
||||
| Hills | Brick | 3 |
|
||||
| Forest | Lumber | 4 |
|
||||
| Pasture | Wool | 4 |
|
||||
| Fields | Grain | 4 |
|
||||
| Mountains | Ore | 3 |
|
||||
| Desert | None | 1 |
|
||||
|
||||
Each non-desert hex has a **number chit** (2–12) placed on it. When that number is rolled on the dice, every player with a settlement or city on that hex receives resources. The Desert has no number and produces nothing. It is useless. Much like small talk at a diplomatic function.
|
||||
|
||||
The board also has **9 ports** along its edges. Ports allow you to trade resources at better rates (2:1 for resource-specific ports, 3:1 for generic ports). The desert hex always sits in the center.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
There are five resource types: **brick, lumber, wool, grain, and ore**. You collect them from hexes bordering your buildings, and you spend them to build things. Your species' entire economy revolves around these five items. Fascinating.
|
||||
|
||||
---
|
||||
|
||||
## Building Costs
|
||||
|
||||
| What You Build | Cost |
|
||||
|----------------|------|
|
||||
| Road | 1 brick + 1 lumber |
|
||||
| Settlement | 1 brick + 1 lumber + 1 wool + 1 grain |
|
||||
| City | 2 grain + 3 ore |
|
||||
| Development Card | 1 wool + 1 grain + 1 ore |
|
||||
|
||||
*Note: Your species has some strange priorities. Upgrading to a city costs very little by comparison to building a settlement from scratch. On my world, we would build the most advanced structure first.*
|
||||
|
||||
---
|
||||
|
||||
## Game Setup
|
||||
|
||||
1. **Shuffle** the terrain tiles (or use the standard beginner layout) and arrange them in the classic Catan pattern. Place number chits on each non-desert hex (randomly, unless using the standard layout). Shuffle and place the nine ports on the coastal edges.
|
||||
|
||||
2. **Determine turn order.** In this game, players place buildings in a "snake" pattern: you go out, then come back. This is your species' way of being "fair." How adorable.
|
||||
|
||||
3. **Each player places two settlements and two roads:**
|
||||
- First round: Place a settlement on an empty node (vertex), then build a road connecting from it.
|
||||
- Second round (reverse order): Place another settlement and road.
|
||||
- Settlements must be at least two nodes apart from each other (the "distance rule").
|
||||
- During the second round, you receive resources from all adjacent hexes (except the Desert).
|
||||
|
||||
4. **The player who placed last in the second round goes first.** Your species' sense of "last = first" is an interesting quirk of your psychology.
|
||||
|
||||
---
|
||||
|
||||
## The Turn Structure
|
||||
|
||||
Each turn has three phases:
|
||||
|
||||
### Phase 1: Rolling the Dice
|
||||
|
||||
The active player rolls two six-sided dice. The total determines resource production:
|
||||
|
||||
- If the total is **7**, **nothing is produced**. Instead:
|
||||
1. Any player with **more than 7 resources** must discard half (rounded down).
|
||||
2. The player moves the **Robber** to a new hex.
|
||||
3. The player may then **steal one random resource** from each opponent who has a settlement or city adjacent to that hex (and who has at least one resource to steal).
|
||||
|
||||
The number 7 is specially designed to be disruptive. Your species loves chaos.
|
||||
|
||||
- If the total is **any other number** (2–12), every settlement on a hex with that number produces **1 resource**, and every city produces **2 resources**. Resources are taken from the bank.
|
||||
|
||||
Some numbers are more valuable than others. The numbers **6 and 8** have the highest probability (5 out of 12 combinations) and are therefore the most sought after. The numbers **2 and 12** have the lowest probability (1 out of 12) and are largely ignored. Even your species' dice have a hierarchy. Predictable.
|
||||
|
||||
### Phase 2: Acting
|
||||
|
||||
After rolling (or if you chose not to roll, which is rare), you may perform **one or more** of the following actions, in any order:
|
||||
|
||||
- **Build a road** (cost: 1 brick + 1 lumber). Roads must connect to one of your existing roads or settlements.
|
||||
- **Build a settlement** (cost: 1 brick + 1 lumber + 1 wool + 1 grain). Must be on an empty node with no other building within two nodes, and must connect to one of your roads.
|
||||
- **Build a city** (cost: 2 grain + 3 ore). Upgrades one of your existing settlements.
|
||||
- **Buy a development card** (cost: 1 wool + 1 grain + 1 ore). These cards are kept secret and can be played on future turns.
|
||||
- **Play a development card** (more on these below).
|
||||
- **Trade resources** with the bank (using ports for better rates) or with other players (negotiation required — your species is surprisingly good at this).
|
||||
|
||||
You may take as many of these actions as your resources allow. There is no limit on the number of actions per turn. The only limit is your resource gathering, which is entirely dependent on your dice luck. How… random.
|
||||
|
||||
### Phase 3: Ending Your Turn
|
||||
|
||||
When you are done, click **End Turn**. The role of active player passes to the next player in turn order.
|
||||
|
||||
---
|
||||
|
||||
## Development Cards
|
||||
|
||||
Development cards are drawn from a deck of 25 cards:
|
||||
|
||||
| Card Type | Quantity | Effect |
|
||||
|-----------|----------|--------|
|
||||
| Knight | 14 | Move the Robber and steal a resource. Counts toward Largest Army. |
|
||||
| Victory Point | 5 | Adds 1 hidden victory point to your score. |
|
||||
| Road Building | 2 | Build two free roads. |
|
||||
| Year of Plenty | 2 | Take any two resources from the bank. |
|
||||
| Monopoly | 2 | Take ALL resources of one type from all other players. |
|
||||
|
||||
**Important rules about development cards:**
|
||||
- You **cannot** play a development card on the same turn you buy it. You must wait until a future turn.
|
||||
- You can play **only one** development card per turn.
|
||||
- Knights count toward **Largest Army** — the first player to play 3 Knights wins this achievement (2 victory points).
|
||||
- Victory Point cards are **hidden** from other players until you reveal them (typically when you play for the win).
|
||||
|
||||
The development card deck heavily favors Knights. This is your species' preferred method of conflict: send knights to steal from each other. How medieval.
|
||||
|
||||
---
|
||||
|
||||
## Trading
|
||||
|
||||
You can trade in three ways:
|
||||
|
||||
### 1. Trade with the Bank (4:1)
|
||||
At a default rate, you may trade **4 of any one resource** for **1 of any other resource**. This is a terrible deal, but it exists.
|
||||
|
||||
### 2. Trade via Ports (3:1 or 2:1)
|
||||
If you have a settlement or city adjacent to a port, you get better rates:
|
||||
- **Resource-specific ports** (2:1): Trade 2 of the port's resource for 1 of any resource.
|
||||
- **Generic ports** (3:1): Trade 3 of any resource for 1 of any other resource.
|
||||
|
||||
Ports are strategically very important. Build near them. I would have noted this in my interstellar trade agreements.
|
||||
|
||||
### 3. Trade with Other Players
|
||||
Players may negotiate resource trades with each other. This is where human psychology comes into play — bluffing, bargaining, betrayal. Your species has been doing this for millennia. I have done it with the Z'xar Hegemony for eons. We are not so different.
|
||||
|
||||
---
|
||||
|
||||
## Special Achievements
|
||||
|
||||
### Longest Road (2 Victory Points)
|
||||
Achieved by building a **continuous road of at least 5 segments**. The road must be unbroken — no opponent's settlement or city can block it. If someone else claims it, you must build a longer road to take it back.
|
||||
|
||||
### Largest Army (2 Victory Points)
|
||||
Achieved by playing **3 Knight development cards**. Like Longest Road, this can be stolen by other players who play more Knights.
|
||||
|
||||
Both achievements are dynamic — they can change hands during the game. Stay alert.
|
||||
|
||||
---
|
||||
|
||||
## The Robber
|
||||
|
||||
The **Robber** starts on the Desert hex. It is moved whenever:
|
||||
- A 7 is rolled (after the discard phase), or
|
||||
- A Knight development card is played
|
||||
|
||||
The Robber **blocks resource production** on its hex — no one gets resources from that terrain while the Robber sits there. Place it on hexes where your opponents have valuable buildings. This is a form of warfare, albeit a subtle one.
|
||||
|
||||
---
|
||||
|
||||
## Victory
|
||||
|
||||
The first player to reach **10 victory points** wins immediately. This can happen at any point — after building, after playing a card, after revealing hidden VP cards. There is no "final round." When you have enough, you win. Declare it. Your species' games are surprisingly straightforward in this regard.
|
||||
|
||||
---
|
||||
|
||||
## Steve's Strategic Notes (From One Intelligent Being to Another)
|
||||
|
||||
1. **Dice probability matters.** Hexes with 6 and 8 are the most productive. Place your buildings there. The numbers 2 and 12 are nearly useless — avoid them unless you have no choice.
|
||||
|
||||
2. **Ports are underrated.** A 2:1 port effectively halves your trading costs. Build settlements near them. This is basic resource management.
|
||||
|
||||
3. **Development cards can swing the game.** Year of Plenty and Monopoly are powerful. Knights serve dual purposes (robbing + army). Victory Point cards are sneaky — hoard them until you need them to win.
|
||||
|
||||
4. **Don't forget the distance rule.** Settlements must be two nodes apart. This limits early-game expansion and forces competition for prime hexes. Plan accordingly.
|
||||
|
||||
5. **Trading is key.** Even with perfect placement, you will sometimes lack the right resources. Learn to trade. Learn to negotiate. Your species invented diplomacy — use it.
|
||||
|
||||
6. **Watch your opponents.** Keep track of their development cards, their road length, and their army count. They may be closer to winning than they appear. Your species tends to focus on itself and neglect others. A fatal flaw.
|
||||
|
||||
---
|
||||
|
||||
*And that, dear Earthlings, is Settlers of Catan. I have explained it thoroughly, as is my duty. I trust you will manage to enjoy this modest diversion during my extended stay. I will be returning to my ship shortly — the void calls, and your planet's atmosphere is… adequate. Good luck. You will need it.*
|
||||
|
||||
*- Steve, Interstellar Traveler*
|
||||
|
|
@ -8,21 +8,20 @@
|
|||
// • delay — "thinking" time [lo, hi] ms (pacing, not intelligence)
|
||||
//
|
||||
// Empirically calibrated against 900 games on the curated answer pool:
|
||||
// Skill 1: ~35% win rate — random opener, frequently ignores revealed info. Very beatable.
|
||||
// Skill 2: ~76% win rate — common-word opener, blunders often, no scoring.
|
||||
// Skill 3: ~93% win rate — decent opener (AUDIO/RAISE tier), light scoring.
|
||||
// Skill 4: ~97% win rate — strong opener (CRANE/SLATE), scores 80 candidates.
|
||||
// Skill 5: ~97%+ win rate — always CRANE, full entropy scan, responds very fast.
|
||||
// The speed difference (skill 5 answers in <1 s) creates real pressure.
|
||||
// Skill 1: ~40% win rate — random opener, almost always ignores revealed info.
|
||||
// Skill 2: ~65% win rate — common-word opener, blunders more than half the time, no scoring.
|
||||
// Skill 3: ~85% win rate — decent opener (AUDIO/RAISE tier), light scoring, frequent mistakes.
|
||||
// Skill 4: ~93% win rate — strong opener (CRANE/SLATE), scores 35 candidates, rare blunders.
|
||||
// Skill 5: ~96% win rate — always CRANE, scores 150 candidates, very fast, occasional blunder.
|
||||
|
||||
import { evaluateGuess } from './WordleLogic.js';
|
||||
|
||||
const SKILL_PROFILES = {
|
||||
1: { openerTier: 0, useFilter: true, blunder: 0.75, maxCandidates: 0, delay: [3800, 6500] },
|
||||
2: { openerTier: 1, useFilter: true, blunder: 0.35, maxCandidates: 0, delay: [3000, 5000] },
|
||||
3: { openerTier: 2, useFilter: true, blunder: 0.10, maxCandidates: 20, delay: [2000, 3600] },
|
||||
4: { openerTier: 3, useFilter: true, blunder: 0.02, maxCandidates: 80, delay: [1100, 2000] },
|
||||
5: { openerTier: 4, useFilter: true, blunder: 0.00, maxCandidates: Infinity, delay: [450, 950] },
|
||||
1: { openerTier: 0, useFilter: true, blunder: 0.85, maxCandidates: 0, delay: [3800, 6500] },
|
||||
2: { openerTier: 1, useFilter: true, blunder: 0.52, maxCandidates: 0, delay: [3000, 5000] },
|
||||
3: { openerTier: 2, useFilter: true, blunder: 0.22, maxCandidates: 10, delay: [2000, 3600] },
|
||||
4: { openerTier: 3, useFilter: true, blunder: 0.08, maxCandidates: 35, delay: [1100, 2000] },
|
||||
5: { openerTier: 4, useFilter: true, blunder: 0.03, maxCandidates: 150, delay: [450, 950] },
|
||||
};
|
||||
|
||||
// Opener tiers — ordered by expected information gain.
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export default class WordleGame extends Phaser.Scene {
|
|||
buildPortraits() {
|
||||
const r = 52;
|
||||
const depth = DEPTH.ui;
|
||||
const py = 90;
|
||||
const py = BOARD_Y + BOARD_H / 2; // vertically centered with the playfield
|
||||
|
||||
const ppx = 120;
|
||||
createPlayerPortrait(this, ppx, py, r, depth, 'WordleGame');
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
playMenuMusic();
|
||||
setMenuMusicVolume(0.25);
|
||||
const cx = GAME_WIDTH / 2;
|
||||
const isCatan = this.gameDef.slug === 'catan';
|
||||
const isCatan = this.gameDef.slug === 'catan';
|
||||
const isGoFish = this.gameDef.slug === 'gofish';
|
||||
const isWordGame = this.gameDef.category === 'word';
|
||||
|
||||
const bgKey = this.gameDef.category === 'casino' ? 'bg-casino' : 'bg-room';
|
||||
this.add.image(cx, GAME_HEIGHT / 2, bgKey).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(-1);
|
||||
|
|
@ -95,7 +97,11 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[opponents[i], opponents[j]] = [opponents[j], opponents[i]];
|
||||
}
|
||||
this.buildOpponentGrid(opponents);
|
||||
const hasOptions = isCatan || isGoFish || this.gameDef.slug === 'dominion'
|
||||
|| (!isWordGame && (this.cache.json.get('playfields')?.playfields?.length > 0))
|
||||
|| this.gameDef.cardGame;
|
||||
const oppScrollH = hasOptions ? OPP_SCROLL_H : 700;
|
||||
this.buildOpponentGrid(opponents, oppScrollH);
|
||||
|
||||
const max = this.gameDef.maxOpponents ?? 1;
|
||||
const defaultCount = this.gameDef.slug === 'nerts' ? 1 : max;
|
||||
|
|
@ -111,13 +117,14 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
|
||||
if (isCatan) this.buildTilePlacementSection(340, 1013);
|
||||
|
||||
const isGoFish = this.gameDef.slug === 'gofish';
|
||||
if (isGoFish) this.buildMatchVariantSection(340, 1013);
|
||||
|
||||
if (this.gameDef.slug === 'dominion') this.buildDeckModeSection(340, 1013);
|
||||
|
||||
this.buildOptionSection('Playfield', 630, this.cache.json.get('playfields')?.playfields ?? [],
|
||||
'selectedPlayfield', 'playfieldTiles', (pf) => this.selectPlayfield(pf));
|
||||
if (!isWordGame) {
|
||||
this.buildOptionSection('Playfield', 630, this.cache.json.get('playfields')?.playfields ?? [],
|
||||
'selectedPlayfield', 'playfieldTiles', (pf) => this.selectPlayfield(pf));
|
||||
}
|
||||
|
||||
if (this.gameDef.cardGame) {
|
||||
this.buildOptionSection('Card Back', 798, this.cache.json.get('card-backs')?.cardBacks ?? [],
|
||||
|
|
@ -126,8 +133,10 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
// Apply playfield default; card back is chosen randomly
|
||||
const pfd = this.cache.json.get('playfields') ?? {};
|
||||
this.applyDefault('playfields', pfd.default, 'selectedPlayfield', 'playfieldTiles');
|
||||
if (!isWordGame) {
|
||||
const pfd = this.cache.json.get('playfields') ?? {};
|
||||
this.applyDefault('playfields', pfd.default, 'selectedPlayfield', 'playfieldTiles');
|
||||
}
|
||||
if (this.gameDef.cardGame) {
|
||||
const cardBacks = this.cache.json.get('card-backs')?.cardBacks ?? [];
|
||||
if (cardBacks.length > 0) {
|
||||
|
|
@ -140,7 +149,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
|
||||
// ── Opponent grid (DOM-based, 2-column scrollable list) ────────────────────
|
||||
|
||||
buildOpponentGrid(opponents) {
|
||||
buildOpponentGrid(opponents, scrollH = OPP_SCROLL_H) {
|
||||
const cx = GAME_WIDTH / 2;
|
||||
const CARD_H = 118;
|
||||
const GAP = 16;
|
||||
|
|
@ -149,7 +158,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = [
|
||||
`width:${OPP_SCROLL_W}px`,
|
||||
`height:${OPP_SCROLL_H}px`,
|
||||
`height:${scrollH}px`,
|
||||
'overflow-y:auto',
|
||||
'overflow-x:hidden',
|
||||
'display:flex',
|
||||
|
|
@ -182,7 +191,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
wrapper.appendChild(row);
|
||||
}
|
||||
|
||||
this.add.dom(cx, OPP_SCROLL_TOP + OPP_SCROLL_H / 2, wrapper);
|
||||
this.add.dom(cx, OPP_SCROLL_TOP + scrollH / 2, wrapper);
|
||||
}
|
||||
|
||||
buildOpponentCardEl(opp, cardH) {
|
||||
|
|
|
|||
|
|
@ -23,11 +23,18 @@ let ran = 0;
|
|||
for (const file of files) {
|
||||
if (applied.has(file)) continue;
|
||||
const sql = fs.readFileSync(path.join(config.db.migrationsDir, file), 'utf8');
|
||||
const tx = db.transaction(() => {
|
||||
db.exec(sql);
|
||||
db.prepare('INSERT INTO schema_migrations (name) VALUES (?)').run(file);
|
||||
});
|
||||
tx();
|
||||
// Disable FK enforcement for the duration of the migration so table-recreation
|
||||
// migrations can drop and rebuild tables that have FK dependents.
|
||||
db.pragma('foreign_keys = OFF');
|
||||
try {
|
||||
const tx = db.transaction(() => {
|
||||
db.exec(sql);
|
||||
db.prepare('INSERT INTO schema_migrations (name) VALUES (?)').run(file);
|
||||
});
|
||||
tx();
|
||||
} finally {
|
||||
db.pragma('foreign_keys = ON');
|
||||
}
|
||||
console.log(`[migrate] applied ${file}`);
|
||||
ran += 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
-- Extend the games.category CHECK constraint to include 'word'.
|
||||
-- SQLite requires table recreation to change a CHECK constraint.
|
||||
|
||||
CREATE TABLE games_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
category TEXT NOT NULL CHECK (category IN ('tabletop', 'casino', 'word')),
|
||||
max_players INTEGER NOT NULL DEFAULT 2,
|
||||
supports_multiplayer INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
INSERT INTO games_new SELECT * FROM games;
|
||||
|
||||
DROP TABLE games;
|
||||
|
||||
ALTER TABLE games_new RENAME TO games;
|
||||
Loading…
Reference in New Issue