Compare commits

..

No commits in common. "e68e21284ce42d9982c6340e323b96b0354db683" and "27576de6cfc14ae548b5b895e6d22a7c8ea05547" have entirely different histories.

36 changed files with 26262 additions and 28477 deletions

View File

@ -3,5 +3,6 @@
"packages/*" "packages/*"
], ],
"version": "independent", "version": "independent",
"npmClient": "yarn",
"useWorkspaces": true "useWorkspaces": true
} }

27335
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,9 +7,9 @@
], ],
"scripts": {}, "scripts": {},
"devDependencies": { "devDependencies": {
"lerna": "^6.6.1" "lerna": "^4.0.0"
}, },
"resolutions": { "resolutions": {
"acorn": "^8.5.0" "acorn": "^8.2.4"
} }
} }

View File

@ -4,20 +4,19 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^12.1.2", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.1.9",
"@types/jest": "^27.0.2", "@types/jest": "^26.0.15",
"@types/node": "^16.11.4", "@types/node": "^12.0.0",
"@types/react": "^17.0.5", "@types/react": "^17.0.5",
"@types/react-dom": "^17.0.5", "@types/react-dom": "^17.0.5",
"eslint-config-react-app": "^6.0.0",
"example-common": "^0.1.1",
"ketchup-react": "^0.1.1", "ketchup-react": "^0.1.1",
"react": "^17.0.2", "react": "^17.0.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.1",
"react-scripts": "^5.0.1", "react-scripts": "^4.0.3",
"typescript": "^4.1.2", "example-common": "^0.1.1",
"web-vitals": "^2.1.2" "typescript": "^4.0.3",
"web-vitals": "^1.1.2"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@ -11,17 +11,17 @@
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/jest": "^27.0.2", "@types/jest": "^26.0.20",
"@types/lodash": "^4.14.176", "@types/lodash": "^4.14.168",
"@types/node": "^16.11.4", "@types/node": "^14.14.22",
"@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^5.1.0", "@typescript-eslint/parser": "^4.14.1",
"eslint": "^8.1.0", "eslint": "^7.18.0",
"jest": "^27.3.1", "jest": "^26.6.3",
"ts-jest": "^27.0.7", "ts-jest": "^26.5.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"
}, },
"scripts": { "scripts": {
"build-lib": "tsc --declaration" "build": "tsc --declaration"
} }
} }

View File

@ -13,7 +13,6 @@ COPY package.json yarn.lock lerna.json ./
RUN lerna bootstrap RUN lerna bootstrap
COPY . ./ COPY . ./
RUN lerna run build-lib
RUN lerna run build RUN lerna run build
ENTRYPOINT ["tini", "--"] ENTRYPOINT ["tini", "--"]

View File

@ -6,23 +6,23 @@
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"dependencies": { "dependencies": {
"example-common": "^0.1.1",
"ketchup-server": "^0.1.1", "ketchup-server": "^0.1.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"ts-node": "^10.4.0", "example-common": "^0.1.1",
"ws": "^8.2.3" "ts-node": "^9.1.1",
"ws": "^7.4.2"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/jest": "^27.0.2", "@types/jest": "^26.0.20",
"@types/lodash": "^4.14.176", "@types/lodash": "^4.14.168",
"@types/node": "^16.11.4", "@types/node": "^14.14.22",
"@types/ws": "^8.2.0", "@types/ws": "^7.4.0",
"@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^5.1.0", "@typescript-eslint/parser": "^4.14.1",
"eslint": "^8.1.0", "eslint": "^7.18.0",
"jest": "^27.3.1", "jest": "^26.6.3",
"ts-jest": "^27.0.7", "ts-jest": "^26.5.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -11,16 +11,16 @@
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/jest": "^27.0.2", "@types/jest": "^26.0.20",
"@types/node": "^16.11.4", "@types/node": "^14.14.22",
"@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^5.1.0", "@typescript-eslint/parser": "^4.14.1",
"eslint": "^8.1.0", "eslint": "^7.18.0",
"jest": "^27.3.1", "jest": "^26.6.3",
"ts-jest": "^27.0.7", "ts-jest": "^26.5.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"
}, },
"scripts": { "scripts": {
"build-lib": "tsc --declaration" "build": "tsc --declaration"
} }
} }

View File

@ -10,18 +10,18 @@
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/jest": "^27.0.2", "@types/jest": "^26.0.20",
"@types/lodash": "^4.14.176", "@types/lodash": "^4.14.168",
"@types/node": "^16.11.4", "@types/node": "^14.14.22",
"@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^5.1.0", "@typescript-eslint/parser": "^4.14.1",
"eslint": "^8.1.0", "eslint": "^7.18.0",
"jest": "^27.3.1", "jest": "^26.6.3",
"ts-jest": "^27.0.7", "ts-jest": "^26.5.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"
}, },
"scripts": { "scripts": {
"build-lib": "tsc --declaration", "build": "tsc --declaration",
"test": "tsc --declaration && jest" "test": "tsc --declaration && jest"
} }
} }

View File

@ -12,17 +12,17 @@
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/jest": "^27.0.2", "@types/jest": "^26.0.20",
"@types/node": "^16.11.4", "@types/node": "^14.14.22",
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^5.1.0", "@typescript-eslint/parser": "^4.14.1",
"eslint": "^8.1.0", "eslint": "^7.18.0",
"jest": "^27.3.1", "jest": "^26.6.3",
"ts-jest": "^27.0.7", "ts-jest": "^26.5.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"
}, },
"scripts": { "scripts": {
"build-lib": "tsc --declaration" "build": "tsc --declaration"
} }
} }

View File

@ -8,19 +8,19 @@
"dependencies": { "dependencies": {
"ketchup-common": "^0.1.1", "ketchup-common": "^0.1.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"ws": "^8.2.3" "ws": "^7.4.2"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/jest": "^27.0.2", "@types/jest": "^26.0.20",
"@types/lodash": "^4.14.176", "@types/lodash": "^4.14.168",
"@types/node": "^16.11.4", "@types/node": "^14.14.22",
"@types/ws": "^8.2.0", "@types/ws": "^7.4.0",
"@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^5.1.0", "@typescript-eslint/parser": "^4.14.1",
"eslint": "^8.1.0", "eslint": "^7.18.0",
"jest": "^27.3.1", "jest": "^26.6.3",
"ts-jest": "^27.0.7", "ts-jest": "^26.5.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -28,6 +28,6 @@
"utf-8-validate": "^5.0.4" "utf-8-validate": "^5.0.4"
}, },
"scripts": { "scripts": {
"build-lib": "tsc --declaration" "build": "tsc --declaration"
} }
} }

View File

@ -4,23 +4,19 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^12.1.2", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^27.0.2", "@types/jest": "^26.0.15",
"@types/node": "^16.11.4", "@types/node": "^12.0.0",
"@types/react": "^17.0.5", "@types/react": "^17.0.0",
"@types/react-dom": "^17.0.5", "@types/react-dom": "^17.0.0",
"@types/roman-numerals": "^0.3.0",
"classnames": "^2.3.1",
"eslint-config-react-app": "^6.0.0",
"formik": "^2.2.6", "formik": "^2.2.6",
"ketchup-react": "^0.1.1", "ketchup-react": "^0.1.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "^5.0.1", "react-scripts": "4.0.3",
"roman-numerals": "^0.3.2",
"typescript": "^4.1.2", "typescript": "^4.1.2",
"web-vitals": "^2.1.2", "web-vitals": "^1.0.1",
"wonders-common": "^0.1.1" "wonders-common": "^0.1.1"
}, },
"scripts": { "scripts": {
@ -47,4 +43,4 @@
"last 1 safari version" "last 1 safari version"
] ]
} }
} }

View File

@ -11,5 +11,4 @@
.wonder-game-wrapper { .wonder-game-wrapper {
overflow: auto; overflow: auto;
flex: 1;
} }

View File

@ -37,7 +37,7 @@ export default function App() {
return <div className='wonder-outer'> return <div className='wonder-outer'>
<div className='wonder-top-bar'> <div className='wonder-top-bar'>
<UserSelector user={user} selectUser={selectUser} removeUser={removeUser} users={state?.players.map(p => p.name) ?? []} disabled={state !== undefined && state.stage !== 'starting'} /> <UserSelector user={user} selectUser={selectUser} removeUser={removeUser} users={state?.players.map(p => p.name) ?? []} locked={state !== undefined && state.stage !== 'starting'} />
<div> <div>
<button onClick={loadSample} disabled={!state}>Load sample</button> <button onClick={loadSample} disabled={!state}>Load sample</button>
{' '} {' '}

View File

@ -1,5 +1,6 @@
.wonder-civ { .wonder-civ {
border: black solid; border: black solid;
margin: 1em;
background-color: white; background-color: white;
} }
@ -18,10 +19,8 @@
.wonder-civ-struct-group { .wonder-civ-struct-group {
list-style-type: none; list-style-type: none;
padding-left: 0; padding-left: 0;
margin-top: 0.4em;
margin-bottom: 0.4em;
font-size: 0.8em; font-size: 0.8em;
width: 13em; width: 12em;
} }
.wonder-civ-struct { .wonder-civ-struct {
border: thin black solid; border: thin black solid;
@ -34,10 +33,3 @@
.wonder-civ-struct-type-advanced-industry { background-color: lightgray; color: black; } .wonder-civ-struct-type-advanced-industry { background-color: lightgray; color: black; }
.wonder-civ-struct-type-guild { background-color: purple; color: white; } .wonder-civ-struct-type-guild { background-color: purple; color: white; }
.wonder-civ-struct-type-military { background-color: red; color: white; } .wonder-civ-struct-type-military { background-color: red; color: white; }
.wonder-civ-struct-age {
float: right;
font-size: 0.6em;
padding-left: 0.2em;
padding-right: 0.2em;
font-family: serif;
}

View File

@ -2,8 +2,6 @@ import { useMemo } from 'react'
import { Player, playerStats, Resource, Science, Structure, StructureType, wonders } from 'wonders-common' import { Player, playerStats, Resource, Science, Structure, StructureType, wonders } from 'wonders-common'
import fill from 'lodash/fill' import fill from 'lodash/fill'
import './Civ.css' import './Civ.css'
import classNames from 'classnames'
import { toRoman } from 'roman-numerals'
type DisplayStyle = 'player' | 'neighbor' | 'distant' type DisplayStyle = 'player' | 'neighbor' | 'distant'
@ -56,27 +54,6 @@ function structurePeek(struct: Structure): string {
} }
} }
function StructGroup({ pStats, displayStyle, type }: { pStats: ReturnType<typeof playerStats>, displayStyle: DisplayStyle, type: StructureType }) {
const structs = pStats.structObjs.filter(o => o.structure.type === type).sort((a, b) =>
a.age === b.age ? a.structure.name.localeCompare(b.structure.name) : a.age - b.age
)
if (displayStyle === 'distant') {
return <div className={classNames({ 'wonder-civ-struct': true, [`wonder-civ-struct-type-${type.replaceAll(' ', '-')}`]: true })}>
{structs.length}
</div>
}
return <ol key={type} className='wonder-civ-struct-group'>
{structs.map((s, i) =>
<li key={i} className={classNames({ 'wonder-civ-struct': true, [`wonder-civ-struct-type-${type.replaceAll(' ', '-')}`]: true })}>
{s.structure.name} {structurePeek(s.structure)}
<div className='wonder-civ-struct-age'>{toRoman(s.age + 1)}</div>
</li>
)}
</ol>
}
export default function Civ({ player, displayStyle }: { player: Player, displayStyle: DisplayStyle }) { export default function Civ({ player, displayStyle }: { player: Player, displayStyle: DisplayStyle }) {
const pStats = useMemo(() => playerStats(player), [player]) const pStats = useMemo(() => playerStats(player), [player])
@ -92,8 +69,14 @@ export default function Civ({ player, displayStyle }: { player: Player, displayS
<div className='wonder-civ-structs'> <div className='wonder-civ-structs'>
{structTypeLayout.get(displayStyle)!.map((col, i) => {structTypeLayout.get(displayStyle)!.map((col, i) =>
<div key={i} className='wonder-civ-struct-col'> <div key={i} className='wonder-civ-struct-col'>
{col.filter(type => pStats.structObjs.some(s => s.structure.type === type)).map((type, i) => {col.filter(type => pStats.structObjs.some(s => s.type === type)).map(type =>
<StructGroup key={i} pStats={pStats} displayStyle={displayStyle} type={type} /> <ol key={type} className='wonder-civ-struct-group'>
{pStats.structObjs.filter(o => o.type === type).map((s, i) =>
<li key={i} className={`wonder-civ-struct wonder-civ-struct-type-${type.replaceAll(' ', '-')}`}>
{s.name} {structurePeek(s)}
</li>
)}
</ol>
)} )}
</div> </div>
)} )}

View File

@ -2,29 +2,21 @@
background-image: url(world.webp); background-image: url(world.webp);
background-position: center; background-position: center;
background-size: cover; background-size: cover;
height: 100%;
} }
.wonder-lobby { .wonder-lobby {
display: flex; display: flex;
flex-wrap: wrap; flex-flow: row wrap;
} }
.wonder-distant { .wonder-distant {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.wonder-neighbors { .wonder-neighbors {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.wonder-neighbor {
display: flex;
flex-flow: column;
justify-content: space-evenly;
}
.wonder-center { .wonder-center {
align-self: center; align-self: center;
} }

View File

@ -4,9 +4,6 @@ import Civ from '../Civ/Civ'
import Card from '../Card/Card' import Card from '../Card/Card'
import './Game.css' import './Game.css'
import WonderSelector from '../WonderSelector/WonderSelector' import WonderSelector from '../WonderSelector/WonderSelector'
import War from '../War/War'
import first from 'lodash/first'
import last from 'lodash/last'
function getDistant<T>(arr: T[], idx: number) { function getDistant<T>(arr: T[], idx: number) {
switch (idx) { switch (idx) {
@ -48,32 +45,19 @@ export default function Game({ state, playerName, dispatch }: { state: State, pl
</> </>
} }
} else { } else {
console.log(player.name, player.leftBattles)
console.log(started.right.name, started.right.rightBattles)
gameElem = <> gameElem = <>
<div className='wonder-distant'> <div className='wonder-distant'>
<War vertical players={[started.left, first(started.distant)!]} state={state} alignBottom /> {started.distant.map(p =>
{started.distant.map((p, i) => <Civ key={p.name} player={p} displayStyle='distant' />
<>
<Civ key={p.name} player={p} displayStyle='distant' />
{i + 1 < started.distant.length && <War vertical players={started.distant.slice(i, i + 2)} state={state} />}
</>
)} )}
<War vertical players={[last(started.distant)!, started.right]} state={state} alignBottom />
</div> </div>
<div className='wonder-neighbors'> <div className='wonder-neighbors'>
<div className='wonder-neighbor'> <Civ player={started.left} displayStyle='neighbor' />
<Civ player={started.left} displayStyle='neighbor' />
<War players={[started.left, player]} state={state} />
</div>
<div className='wonder-center'> <div className='wonder-center'>
<div>{state.age === undefined ? '' : `Age ${state.age + 1} ${state.age % 2 === 0 ? '🔃' : '🔄'}`}</div> <div>{state.age === undefined ? '' : `Age ${state.age + 1} ${state.age % 2 === 0 ? '🔃' : '🔄'}`}</div>
<div><button>Discards</button></div> <div><button>Discards</button></div>
</div> </div>
<div className='wonder-neighbor'> <Civ player={started.right} displayStyle='neighbor' />
<Civ player={started.right} displayStyle='neighbor' />
<War players={[started.right, player]} state={state} />
</div>
</div> </div>
<Civ player={player} displayStyle='player' /> <Civ player={player} displayStyle='player' />
<div>{player.hand?.map(s => structures.get(s)!)?.map((s, i) => <div>{player.hand?.map(s => structures.get(s)!)?.map((s, i) =>

View File

@ -14,13 +14,13 @@ const namePlaceholders = [
'Tutankhamen', 'Tutankhamen',
] ]
export default function UserSelector({ users, user, selectUser, removeUser, disabled }: { export default function UserSelector({ users, user, selectUser, removeUser, locked }: {
users: string[], user?: string, selectUser: Dispatch<string | undefined>, removeUser: () => void, disabled?: boolean users: string[], user?: string, selectUser: Dispatch<string | undefined>, removeUser: () => void, locked: boolean
}) { }) {
const [namePlaceholder] = useState(() => sample(namePlaceholders)) const [namePlaceholder] = useState(() => sample(namePlaceholders))
return <div> return <div>
{user {user
? <>Playing as {user} <button onClick={() => selectUser(undefined)}>Change</button> <button onClick={removeUser} disabled={disabled}>Remove</button></> ? <>Playing as {user} <button onClick={() => selectUser(undefined)}>Change</button> <button onClick={removeUser} disabled={locked}>Remove</button></>
: <Formik : <Formik
initialValues={{ newUser: '' }} initialValues={{ newUser: '' }}
validate={values => { validate={values => {

View File

@ -1,46 +0,0 @@
.wonder-war {
display: flex;
background-color: lightcoral;
align-self: center;
justify-content: center;
font-family: serif;
text-align: center;
}
.wonder-war-vertical {
flex-direction: column;
}
.wonder-war-crossed-swords {
align-self: center;
}
.wonder-war-align-bottom {
align-self: flex-end;
}
.wonder-war-battle {
border: 1pt solid red;
margin: 0.1em;
padding: 0.1em;
display: flex;
align-items: center;
}
.wonder-war-vertical .wonder-war-battle {
flex-direction: column;
}
.wonder-war-battle-results {
display: flex;
justify-content: space-between;
}
.wonder-war-horizontal .wonder-war-battle-results {
flex-direction: column;
}
.wonder-war-battle-result {
background-color: red;
color: white;
margin: 0.2em;
min-width: 1.5em;
font-weight: bold;
}

View File

@ -1,30 +0,0 @@
import classNames from "classnames";
import { numAges, Player, State } from 'wonders-common'
import zip from 'lodash/zip'
import range from 'lodash/range'
import './War.css'
import { toRoman } from 'roman-numerals'
function Battle({ age, future, battle }: { age: number, future: boolean, battle: [number | undefined, number | undefined] }) {
function resultText(result?: number) {
return future ? '…' : result ?? '🕊️'
}
return <div className='wonder-war-battle'>
<div>{toRoman(age + 1)}</div>
<div className='wonder-war-battle-results'>
{range(2).map(i => <div className='wonder-war-battle-result' key={i}>{resultText(battle[i])}</div>)}
</div>
</div>
}
export default function War({ state, players, vertical, alignBottom }: { state: State, players: Player[], vertical?: boolean, alignBottom?: boolean }) {
let battles = zip(players[0].rightBattles, players[1].leftBattles)
console.log(players, vertical, battles)
return <div className={classNames({ 'wonder-war': true, 'wonder-war-vertical': vertical, 'wonder-war-horizontal': !vertical, 'wonder-war-align-bottom': alignBottom })}>
<div className='wonder-war-crossed-swords'></div>
{range(numAges).map(age => <Battle age={age} battle={battles[age] ?? []} future={age >= state.age!} key={age} />)}
</div>
}

File diff suppressed because it is too large Load Diff

View File

@ -11,17 +11,17 @@
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node14": "^1.0.0", "@tsconfig/node14": "^1.0.0",
"@types/jest": "^27.0.2", "@types/jest": "^26.0.20",
"@types/lodash": "^4.14.176", "@types/lodash": "^4.14.168",
"@types/node": "^16.11.4", "@types/node": "^14.14.22",
"@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^5.1.0", "@typescript-eslint/parser": "^4.14.1",
"eslint": "^8.1.0", "eslint": "^7.18.0",
"jest": "^27.3.1", "jest": "^26.6.3",
"ts-jest": "^27.0.7", "ts-jest": "^26.5.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"
}, },
"scripts": { "scripts": {
"build-lib": "tsc --declaration" "build": "tsc --declaration"
} }
} }

View File

@ -1,83 +0,0 @@
import shuffle from 'lodash/shuffle'
import { TurnPlan } from './basic'
export interface BaseAction {
type: string
}
export interface AddPlayerAction extends BaseAction {
type: 'add player'
name: string
}
export function addPlayerAction(name: string): AddPlayerAction {
return {
type: 'add player',
name,
}
}
export interface RemovePlayerAction extends BaseAction {
type: 'remove player'
name: string
}
export function removePlayerAction(name: string): RemovePlayerAction {
return {
type: 'remove player',
name,
}
}
export interface ChooseWonderAction extends BaseAction {
type: 'choose wonder'
player: string
wonder: string
}
export function chooseWonderAction(player: string, wonder: string): ChooseWonderAction {
return {
type: 'choose wonder',
player,
wonder,
}
}
export interface StartGameAction extends BaseAction {
type: 'start game'
shuffle: number[]
}
export function startGameAction(numPlayers: number): StartGameAction {
return {
type: 'start game',
shuffle: shuffle([...Array(numPlayers).keys()]),
}
}
export interface PlanTurnAction extends BaseAction {
type: 'plan turn'
player: string
turnPlan?: TurnPlan
}
export function planTurnAction(player: string, turnPlan?: TurnPlan): PlanTurnAction {
return {
type: 'plan turn',
player,
turnPlan,
}
}
export interface ResetAction extends BaseAction {
type: 'reset'
}
export function resetAction(): ResetAction {
return { type: 'reset' }
}
export interface LoadSampleAction extends BaseAction {
type: 'load sample'
}
export function loadSampleAction(): LoadSampleAction {
return { type: 'load sample' }
}
export type Action = AddPlayerAction | RemovePlayerAction | ChooseWonderAction | StartGameAction
| PlanTurnAction
| ResetAction | LoadSampleAction

View File

@ -1,181 +0,0 @@
export const numAges = 3
export const minPlayers = 3
export const maxPlayers = 7
export const startingGold = 3
export const discardGold = 3
export const victoryPoints = [1, 3, 5]
export const defeatPoints = -1
export const goldPerPoint = 3
export const scienceSetPoints = 7
export type Resource = 'wood' | 'stone' | 'ore' | 'brick' | 'paper' | 'cloth' | 'glass'
export const resources: Resource[] = ['wood', 'stone', 'ore', 'brick', 'paper', 'cloth', 'glass']
export type Science = 'engineering' | 'research' | 'education'
export const sciences: Science[] = ['engineering', 'research', 'education']
export interface TurnPlan {
action: 'structure' | 'discard' | 'wonder'
cardIdx: number
goldLeft: number
goldRight: number
}
export interface Player {
name: string
wonder?: string
gold: number
structures: {
age: number
structure: string
}[]
hand: string[]
turnPlan?: TurnPlan
leftBattles: number[]
rightBattles: number[]
}
export interface State {
stage: 'starting' | 'play' | 'finished'
players: Player[]
age?: number
turnsRemaining?: number
discard: string[]
}
export const initial: State = {
stage: 'starting',
players: [],
discard: [],
}
export const sampleState: State = {
stage: 'play',
age: 2,
turnsRemaining: 4,
discard: [
'Scriptorium', 'Scriptorium', 'Guard Tower', 'Baths', 'Stone Pit',
'Caravansery', 'Loom', 'Loom', 'Stables', 'Courthouse',
'Craftsmens Guild', 'Strategists Guild', 'Pantheon', 'Palace', 'Chamber of Commerce'
],
players: [
{
name: 'Player A',
gold: 2,
leftBattles: [1, 3],
rightBattles: [1, 3],
wonder: 'Mausoleum at Halicarnassus B',
hand: ['Study', 'Arsenal', 'Haven', 'Haven'],
structures: [
{ age: 0, structure: 'Stockade' },
{ age: 0, structure: 'Barracks' },
{ age: 0, structure: 'Barracks' },
{ age: 0, structure: 'Glassworks' },
{ age: 0, structure: 'Stone Pit' },
{ age: 0, structure: 'Lumber Yard' },
{ age: 1, structure: 'Walls' },
{ age: 1, structure: 'Bazar' },
{ age: 1, structure: 'Caravansery' },
{ age: 1, structure: 'Forum' },
{ age: 1, structure: 'Aqueduct' },
{ age: 1, structure: 'Quarry' },
{ age: 2, structure: 'Arena' },
{ age: 2, structure: 'Fortifications' },
]
}, {
name: 'Player B',
gold: 5,
leftBattles: [-1, -1],
rightBattles: [-1, -1],
wonder: 'Temple of Artemis at Ephesus B',
hand: ['Town Hall', 'University', 'Arsenal', 'Circus'],
structures: [
{ age: 0, structure: 'Apothecary' },
{ age: 0, structure: 'Pawnshop' },
{ age: 0, structure: 'Timber Yard' },
{ age: 0, structure: 'Press' },
{ age: 0, structure: 'Ore Vein' },
{ age: 0, structure: 'Clay Pool' },
{ age: 1, structure: 'Courthouse' },
{ age: 1, structure: 'Statue' },
{ age: 1, structure: 'Laboratory' },
{ age: 1, structure: 'Library' },
{ age: 1, structure: 'Sawmill' },
{ age: 1, structure: 'Foundry' },
{ age: 2, structure: 'Lighthouse' },
{ age: 2, structure: 'Builders Guild' },
]
}, {
name: 'Player C',
gold: 3,
leftBattles: [1, 3],
rightBattles: [1, 3],
wonder: 'Hanging Gardens of Babylon B',
hand: ['Town Hall', 'Magistrates Guild', 'Scientists Guild', 'Workers Guild'],
structures: [
{ age: 0, structure: 'Clay Pool' },
{ age: 0, structure: 'Forest Cave' },
{ age: 0, structure: 'Loom' },
{ age: 0, structure: 'Ore Vein' },
{ age: 0, structure: 'Guard Tower' },
{ age: 0, structure: 'East Trading Post' },
{ age: 1, structure: 'Laboratory' },
{ age: 1, structure: 'Archery Range' },
{ age: 1, structure: 'Dispensary' },
{ age: 1, structure: 'Glassworks' },
{ age: 1, structure: 'Press' },
{ age: 1, structure: 'Glassworks' },
{ age: 2, structure: 'Siege Workshop' },
{ age: 2, structure: 'Philosophers Guild' },
]
}, {
name: 'Player D',
gold: 10,
leftBattles: [-1, -1],
rightBattles: [],
wonder: 'Colossus of Rhodes A',
hand: ['Senate', 'Study', 'Senate', 'Arena'],
structures: [
{ age: 0, structure: 'Excavation' },
{ age: 0, structure: 'Lumber Yard' },
{ age: 0, structure: 'Clay Pit' },
{ age: 0, structure: 'Workshop' },
{ age: 0, structure: 'Tavern' },
{ age: 0, structure: 'West Trading Post' },
{ age: 1, structure: 'Temple' },
{ age: 1, structure: 'Vineyard' },
{ age: 1, structure: 'Stables' },
{ age: 1, structure: 'Press' },
{ age: 1, structure: 'Quarry' },
{ age: 1, structure: 'Brickyard' },
{ age: 2, structure: 'Gardens' },
{ age: 2, structure: 'Circus' },
]
}, {
name: 'Player E',
gold: 0,
leftBattles: [],
rightBattles: [-1, -1],
wonder: 'Great Pyramid of Giza A',
hand: ['Observatory', 'Academy', 'University', 'Lodge'],
structures: [
{ age: 0, structure: 'Apothecary' },
{ age: 0, structure: 'Tavern' },
{ age: 0, structure: 'Altar' },
{ age: 0, structure: 'Theater' },
{ age: 0, structure: 'Marketplace' },
{ age: 0, structure: 'Altar' },
{ age: 1, structure: 'Sawmill' },
{ age: 1, structure: 'Foundry' },
{ age: 1, structure: 'Brickyard' },
{ age: 1, structure: 'Dispensary' },
{ age: 1, structure: 'Training Ground' },
{ age: 1, structure: 'School' },
{ age: 2, structure: 'Siege Workshop' },
{ age: 2, structure: 'Gardens' },
]
},
],
}

View File

@ -1,6 +1,694 @@
export * from './basic' import min from 'lodash/min'
export * from './structure' import chunk from 'lodash/chunk'
export * from './wonder' import isInteger from 'lodash/isInteger'
export * from './util' import sampleSize from 'lodash/sampleSize'
export * from './action' import shuffle from 'lodash/shuffle'
export * from './reducer' import sum from 'lodash/sum'
import max from 'lodash/max'
import head from 'lodash/head'
import tail from 'lodash/tail'
import compact from 'lodash/compact'
export const numAges = 3
export const minPlayers = 3
export const maxPlayers = 7
export const startingGold = 3
export const discardGold = 3
export const victoryPoints = [1, 3, 5]
export const defeatPoints = -1
export const goldPerPoint = 3
export const scienceSetPoints = 7
export type Resource = 'wood' | 'stone' | 'ore' | 'brick' | 'paper' | 'cloth' | 'glass'
export const resources: Resource[] = ['wood', 'stone', 'ore', 'brick', 'paper', 'cloth', 'glass']
export type Science = 'engineering' | 'research' | 'education'
export const sciences: Science[] = ['engineering', 'research', 'education']
export type StructureType = 'commerce' | 'culture' | 'science' | 'basic industry' | 'advanced industry' | 'guild' | 'military'
export const structureTypes: StructureType[] = ['commerce', 'culture', 'science', 'basic industry', 'advanced industry', 'guild', 'military']
export type Countable = StructureType | 'wonder stage' | 'defeat'
export interface Structure {
type: StructureType
name: string
appears?: (undefined | (3 | 4 | 5 | 6 | 7)[])[]
cost?: Map<Resource | 'gold', number>
freeWith?: string[]
paysFor?: string[]
gold?: number
goldPer?: Map<Countable, number>
goldPerNeighbor?: Map<Countable, number>
points?: number
pointsPer?: Map<Countable, number>
pointsPerNeighbor?: Map<Countable, number>
sciences?: Set<Science>
resources?: Map<Resource, number>
shields?: number
discountTrade?: {
resourceType: 'basic' | 'advanced'
direction: ('left' | 'right')[]
amount: number
}[]
}
const structuresAge1: Structure[] = [
{ type: 'advanced industry', name: 'Glassworks', appears: [[3, 6], [3, 5]], resources: new Map([['glass', 1]]) },
{ type: 'advanced industry', name: 'Loom', appears: [[3, 6], [3, 5]], resources: new Map([['cloth', 1]]) },
{ type: 'advanced industry', name: 'Press', appears: [[3, 6], [3, 5]], resources: new Map([['paper', 1]]) },
{ type: 'basic industry', name: 'Clay Pit', appears: [[3]], cost: new Map([['gold', 1]]), resources: new Map([['brick', 1], ['ore', 1]]) },
{ type: 'basic industry', name: 'Clay Pool', appears: [[3, 5]], resources: new Map([['brick', 1]]) },
{ type: 'basic industry', name: 'Excavation', appears: [[4]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 1], ['brick', 1]]) },
{ type: 'basic industry', name: 'Forest Cave', appears: [[5]], cost: new Map([['gold', 1]]), resources: new Map([['wood', 1], ['ore', 1]]) },
{ type: 'basic industry', name: 'Lumber Yard', appears: [[3, 4]], resources: new Map([['wood', 1]]) },
{ type: 'basic industry', name: 'Mine', appears: [[6]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 1], ['ore', 1]]) },
{ type: 'basic industry', name: 'Ore Vein', appears: [[3, 4]], resources: new Map([['ore', 1]]) },
{ type: 'basic industry', name: 'Stone Pit', appears: [[3, 5]], resources: new Map([['stone', 1]]) },
{ type: 'basic industry', name: 'Timber Yard', appears: [[3]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 1], ['wood', 1]]) },
{ type: 'basic industry', name: 'Tree Farm', appears: [[6]], cost: new Map([['gold', 1]]), resources: new Map([['wood', 1], ['brick', 1]]) },
{ type: 'commerce', name: 'East Trading Post', appears: [[3, 7]], discountTrade: [{ resourceType: 'basic', direction: ['right'], amount: 1 }] },
{ type: 'commerce', name: 'Marketplace', appears: [[3, 6]], discountTrade: [{ resourceType: 'advanced', direction: ['left', 'right'], amount: 1 }] },
{ type: 'commerce', name: 'Tavern', appears: [[4, 5, 7]], gold: 5 },
{ type: 'commerce', name: 'West Trading Post', appears: [[3, 7]], discountTrade: [{ resourceType: 'basic', direction: ['left'], amount: 1 }] },
{ type: 'culture', name: 'Altar', appears: [[3, 5]], points: 2 },
{ type: 'culture', name: 'Baths', appears: [[3, 7]], cost: new Map([['stone', 1]]), points: 3 },
{ type: 'culture', name: 'Pawnshop', appears: [[4, 7]], points: 3 },
{ type: 'culture', name: 'Theater', appears: [[3, 6]], points: 2 },
{ type: 'military', name: 'Barracks', appears: [[3, 5]], cost: new Map([['ore', 1]]), shields: 1 },
{ type: 'military', name: 'Guard Tower', appears: [[3, 4]], cost: new Map([['brick', 1]]), shields: 1 },
{ type: 'military', name: 'Stockade', appears: [[3, 7]], cost: new Map([['wood', 1]]), shields: 1 },
{ type: 'science', name: 'Apothecary', appears: [[3, 5]], cost: new Map([['cloth', 1]]), sciences: new Set(['research']) },
{ type: 'science', name: 'Scriptorium', appears: [[3, 4]], cost: new Map([['glass', 1]]), sciences: new Set(['education']) },
{ type: 'science', name: 'Workshop', appears: [[3, 7]], cost: new Map([['glass', 1]]), sciences: new Set(['engineering']) },
]
const structuresAge2: Structure[] = [
{ type: 'basic industry', name: 'Brickyard', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['brick', 2]]) },
{ type: 'basic industry', name: 'Foundry', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['ore', 2]]) },
{ type: 'basic industry', name: 'Sawmill', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['wood', 2]]) },
{ type: 'basic industry', name: 'Quarry', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 2]]) },
{ type: 'commerce', name: 'Bazar', appears: [, [4, 7]], goldPer: new Map([['advanced industry', 2]]), goldPerNeighbor: new Map([['advanced industry', 2]]) },
{ type: 'commerce', name: 'Caravansery', appears: [, [3, 5, 6]], cost: new Map([['wood', 2]]), freeWith: ['Marketplace'], resources: new Map([['wood', 1], ['stone', 1], ['ore', 1], ['brick', 1]]) },
{ type: 'commerce', name: 'Forum', appears: [, [3, 6, 7]], cost: new Map([['brick', 2]]), freeWith: ['East Trading Post', 'West Trading Post'], resources: new Map([['glass', 1], ['cloth', 1], ['paper', 1]]) },
{ type: 'commerce', name: 'Vineyard', appears: [, [3, 6]], goldPer: new Map([['basic industry', 1]]), goldPerNeighbor: new Map([['basic industry', 1]]) },
{ type: 'culture', name: 'Aqueduct', appears: [, [3, 7]], cost: new Map([['stone', 3]]), freeWith: ['Baths'], points: 5 },
{ type: 'culture', name: 'Courthouse', appears: [, [3, 5]], cost: new Map([['brick', 2], ['cloth', 1]]), freeWith: ['Scriptorium'], points: 4 },
{ type: 'culture', name: 'Statue', appears: [, [3, 7]], cost: new Map([['ore', 2], ['wood', 1]]), freeWith: ['Theater'], points: 4 },
{ type: 'culture', name: 'Temple', appears: [, [3, 6]], cost: new Map([['brick', 1], ['wood', 1], ['glass', 1]]), freeWith: ['Altar'], points: 3 },
{ type: 'military', name: 'Archery Range', appears: [, [3, 6]], cost: new Map([['wood', 2], ['ore', 1]]), freeWith: ['Workshop'], shields: 2 },
{ type: 'military', name: 'Stables', appears: [, [3, 5]], cost: new Map([['wood', 1], ['ore', 1], ['brick', 1]]), freeWith: ['Apothecary'], shields: 2 },
{ type: 'military', name: 'Training Ground', appears: [, [4, 6, 7]], cost: new Map([['ore', 2], ['wood', 1]]), shields: 2 },
{ type: 'military', name: 'Walls', appears: [, [3, 7]], cost: new Map([['stone', 3]]), shields: 2 },
{ type: 'science', name: 'Dispensary', appears: [, [3, 4]], cost: new Map([['glass', 1], ['ore', 2]]), freeWith: ['Apothecary'], sciences: new Set(['research']) },
{ type: 'science', name: 'Laboratory', appears: [, [3, 5]], cost: new Map([['paper', 1], ['brick', 2]]), freeWith: ['Workshop'], sciences: new Set(['engineering']) },
{ type: 'science', name: 'Library', appears: [, [3, 6]], cost: new Map([['cloth', 1], ['stone', 2]]), freeWith: ['Scriptorium'], sciences: new Set(['education']) },
{ type: 'science', name: 'School', appears: [, [3, 7]], cost: new Map([['paper', 1], ['wood', 1]]), sciences: new Set(['education']) },
]
const structuresAge3: Structure[] = [
{ type: 'commerce', name: 'Arena', appears: [, , [3, 3, 5, 7]], cost: new Map([['stone', 2], ['ore', 1]]), freeWith: ['Dispensary'], goldPer: new Map([['wonder stage', 3]]), pointsPer: new Map([['wonder stage', 1]]) },
{ type: 'commerce', name: 'Chamber of Commerce', appears: [, , [4, 6]], cost: new Map([['brick', 2], ['paper', 1]]), goldPer: new Map([['advanced industry', 2]]), pointsPer: new Map([['advanced industry', 2]]) },
{ type: 'commerce', name: 'Haven', appears: [, , [4]], cost: new Map([['wood', 1], ['ore', 1], ['paper', 1]]), goldPer: new Map([['basic industry', 1]]), pointsPer: new Map([['basic industry', 1]]) },
{ type: 'commerce', name: 'Lighthouse', appears: [, , [3, 6]], cost: new Map([['stone', 1], ['glass', 1]]), freeWith: ['Caravansery'], goldPer: new Map([['commerce', 1]]), pointsPer: new Map([['commerce', 1]]) },
{ type: 'culture', name: 'Gardens', appears: [, , [3, 4]], cost: new Map([['brick', 2], ['wood', 1]]), points: 5 },
{ type: 'culture', name: 'Palace', appears: [, , [3, 7]], cost: new Map([['wood', 1], ['brick', 1], ['stone', 1], ['ore', 1], ['glass', 1], ['cloth', 1], ['paper', 1]]), points: 8 },
{ type: 'culture', name: 'Pantheon', appears: [, , [3, 6]], cost: new Map([['brick', 2], ['ore', 1], ['glass', 1], ['paper', 1], ['cloth', 1]]), freeWith: ['Temple'], points: 7 },
{ type: 'culture', name: 'Senate', appears: [, , [3, 5]], cost: new Map([['wood', 2], ['ore', 1], ['stone', 1]]), freeWith: ['Library'], points: 6 },
{ type: 'culture', name: 'Town Hall', appears: [, , [3, 5, 6]], cost: new Map([['stone', 2], ['ore', 1], ['glass', 1]]), points: 6 },
{ type: 'military', name: 'Arsenal', appears: [, , [3, 4, 7]], cost: new Map([['wood', 2], ['ore', 1], ['cloth', 1]]), shields: 3 },
{ type: 'military', name: 'Circus', appears: [, , [4, 5, 6]], cost: new Map([['stone', 3], ['ore', 1]]), freeWith: ['Training Ground'], shields: 3 },
{ type: 'military', name: 'Fortifications', appears: [, , [3, 7]], cost: new Map([['ore', 3], ['stone', 1]]), freeWith: ['Walls'], shields: 3 },
{ type: 'military', name: 'Siege Workshop', appears: [, , [3, 5]], cost: new Map([['brick', 3], ['wood', 1]]), freeWith: ['Laboratory'], shields: 3 },
{ type: 'science', name: 'Academy', appears: [, , [3, 7]], cost: new Map([['stone', 3], ['glass', 1]]), freeWith: ['School'], sciences: new Set(['research']) },
{ type: 'science', name: 'Lodge', appears: [, , [3, 6]], cost: new Map([['brick', 2], ['paper', 1], ['cloth', 1]]), freeWith: ['Dispensary'], sciences: new Set(['research']) },
{ type: 'science', name: 'Observatory', appears: [, , [3, 7]], cost: new Map([['ore', 2], ['glass', 1], ['cloth', 1]]), freeWith: ['Laboratory'], sciences: new Set(['engineering']) },
{ type: 'science', name: 'Study', appears: [, , [3, 5]], cost: new Map([['wood', 1], ['paper', 1], ['cloth', 1]]), freeWith: ['School'], sciences: new Set(['engineering']) },
{ type: 'science', name: 'University', appears: [, , [3, 4]], cost: new Map([['wood', 2], ['paper', 1], ['glass', 1]]), freeWith: ['Library'], sciences: new Set(['education']) },
]
const structuresGuilds: Structure[] = [
{ type: 'guild', name: 'Builders Guild', cost: new Map([['stone', 2], ['brick', 2], ['glass', 1]]), pointsPer: new Map([['wonder stage', 1]]), pointsPerNeighbor: new Map([['wonder stage', 1]]) },
{ type: 'guild', name: 'Craftsmens Guild', cost: new Map([['ore', 2], ['stone', 2]]), pointsPerNeighbor: new Map([['advanced industry', 2]]) },
{ type: 'guild', name: 'Magistrates Guild', cost: new Map([['wood', 3], ['stone', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['culture', 1]]) },
{ type: 'guild', name: 'Philosophers Guild', cost: new Map([['brick', 3], ['paper', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['science', 1]]) },
{ type: 'guild', name: 'Scientists Guild', cost: new Map([['ore', 2], ['wood', 2], ['paper', 1]]), sciences: new Set(['engineering', 'research', 'education']) },
{ type: 'guild', name: 'Shipowners Guild', cost: new Map([['wood', 3], ['glass', 1], ['paper', 1]]), pointsPer: new Map([['basic industry', 1], ['advanced industry', 1], ['guild', 1]]) },
{ type: 'guild', name: 'Spies Guild', cost: new Map([['brick', 3], ['glass', 1]]), pointsPerNeighbor: new Map([['military', 1]]) },
{ type: 'guild', name: 'Strategists Guild', cost: new Map([['ore', 2], ['stone', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['defeat', 1]]) },
{ type: 'guild', name: 'Traders Guild', cost: new Map([['glass', 1], ['paper', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['commerce', 1]]) },
{ type: 'guild', name: 'Workers Guild', cost: new Map([['ore', 2], ['brick', 1], ['stone', 1], ['wood', 1]]), pointsPerNeighbor: new Map([['basic industry', 1]]) },
]
export const structures = new Map<string, Structure>([structuresAge1, structuresAge2, structuresAge3, structuresGuilds].flat().map(c => ([c.name, c])))
// Initialize 'freeWith' properties
for (const structure of structures.values()) {
for (const precursorName of structure.freeWith ?? []) {
const precursor = structures.get(precursorName)!
if (precursor.paysFor === undefined) {
precursor.paysFor = [structure.name]
} else {
precursor.paysFor.push(structure.name)
}
}
}
export function buildDeck(age: number, numPlayers: number) {
if (age < 0 || age >= numAges || !isInteger(age)) {
throw new Error(`Unsupported age ${age}`)
}
if (numPlayers < minPlayers || numPlayers > maxPlayers || !isInteger(numPlayers)) {
throw new Error(`Unsupported player count ${numPlayers}`)
}
const cards: Structure[] = [...structures.values()].flatMap(structure =>
structure.appears?.[age]
?.filter(minPlayers => numPlayers <= minPlayers)
?.map(() => structure)
?? []
)
if (age === numAges - 1) {
cards.push(...sampleSize(structuresGuilds, numPlayers + 2)) // BAD
}
return cards
}
export interface Wonder {
name: string
innateResource: Resource
//stages: Structure[]
}
const wonderList: Wonder[] = [
{
name: 'Great Pyramid of Giza A',
innateResource: 'stone'
}, {
name: 'Great Pyramid of Giza B',
innateResource: 'stone'
}, {
name: 'Hanging Gardens of Babylon A',
innateResource: 'brick'
}, {
name: 'Hanging Gardens of Babylon B',
innateResource: 'brick'
}, {
name: 'Temple of Artemis at Ephesus A',
innateResource: 'paper'
}, {
name: 'Temple of Artemis at Ephesus B',
innateResource: 'paper'
}, {
name: 'Statue of Zeus at Olympia A',
innateResource: 'wood'
}, {
name: 'Statue of Zeus at Olympia B',
innateResource: 'wood'
}, {
name: 'Mausoleum at Halicarnassus A',
innateResource: 'cloth'
}, {
name: 'Mausoleum at Halicarnassus B',
innateResource: 'cloth'
}, {
name: 'Colossus of Rhodes A',
innateResource: 'ore'
}, {
name: 'Colossus of Rhodes B',
innateResource: 'ore'
}, {
name: 'Lighthouse of Alexandria A',
innateResource: 'glass'
}, {
name: 'Lighthouse of Alexandria B',
innateResource: 'glass'
},
]
export const wonders = new Map(wonderList.map(w => [w.name, w]))
export interface TurnPlan {
action: 'structure' | 'discard'
cardIdx: number
goldLeft: number
goldRight: number
}
export interface Player {
name: string
wonder?: string
gold: number
structures: {
age: number
structure: string
}[]
hand: string[]
turnPlan?: TurnPlan
leftBattles: number[]
rightBattles: number[]
}
export function sumMaps<V>(ms?: (Map<V, number> | undefined)[]) {
const sum = new Map<V, number>()
for (const m of ms ?? []) {
for (const [k, v] of m ?? []) {
sum.set(k, (sum.get(k) ?? 0) + v)
}
}
return sum
}
export function playerStats(p: Player) {
const structObjs = compact(p.structures.map(({ structure }) => structures.get(structure)))
return {
structObjs,
resources: sumMaps(structObjs.map(s => s.resources)),
shields: sum(structObjs.map(s => s.shields)) ?? 0,
sciences: compact(structObjs.map(s => s.sciences)),
}
}
export interface State {
stage: 'starting' | 'play' | 'finished'
players: Player[]
age?: number
turnsRemaining?: number
discard: string[]
}
export const initial: State = {
stage: 'starting',
players: [],
discard: [],
}
export const sampleState: State = {
stage: 'play',
age: 2,
turnsRemaining: 4,
discard: [
'Scriptorium', 'Scriptorium', 'Guard Tower', 'Baths', 'Stone Pit',
'Caravansery', 'Loom', 'Loom', 'Stables', 'Courthouse',
'Craftsmens Guild', 'Strategists Guild', 'Pantheon', 'Palace', 'Chamber of Commerce'
],
players: [
{
name: 'Player A',
gold: 2,
leftBattles: [1, 3],
rightBattles: [1, 3],
wonder: 'Mausoleum at Halicarnassus B',
hand: ['Study', 'Arsenal', 'Haven', 'Haven'],
structures: [
{ age: 0, structure: 'Stockade' },
{ age: 0, structure: 'Barracks' },
{ age: 0, structure: 'Barracks' },
{ age: 0, structure: 'Glassworks' },
{ age: 0, structure: 'Stone Pit' },
{ age: 0, structure: 'Lumber Yard' },
{ age: 1, structure: 'Walls' },
{ age: 1, structure: 'Bazar' },
{ age: 1, structure: 'Caravansery' },
{ age: 1, structure: 'Forum' },
{ age: 1, structure: 'Aqueduct' },
{ age: 1, structure: 'Quarry' },
{ age: 2, structure: 'Arena' },
{ age: 2, structure: 'Fortifications' },
]
}, {
name: 'Player B',
gold: 5,
leftBattles: [-1, -1],
rightBattles: [-1, -1],
wonder: 'Temple of Artemis at Ephesus B',
hand: ['Town Hall', 'University', 'Arsenal', 'Circus'],
structures: [
{ age: 0, structure: 'Apothecary' },
{ age: 0, structure: 'Pawnshop' },
{ age: 0, structure: 'Timber Yard' },
{ age: 0, structure: 'Press' },
{ age: 0, structure: 'Ore Vein' },
{ age: 0, structure: 'Clay Pool' },
{ age: 1, structure: 'Courthouse' },
{ age: 1, structure: 'Statue' },
{ age: 1, structure: 'Laboratory' },
{ age: 1, structure: 'Library' },
{ age: 1, structure: 'Sawmill' },
{ age: 1, structure: 'Foundry' },
{ age: 2, structure: 'Lighthouse' },
{ age: 2, structure: 'Builders Guild' },
]
}, {
name: 'Player C',
gold: 3,
leftBattles: [1, 3],
rightBattles: [1, 3],
wonder: 'Hanging Gardens of Babylon B',
hand: ['Town Hall', 'Magistrates Guild', 'Scientists Guild', 'Workers Guild'],
structures: [
{ age: 0, structure: 'Clay Pool' },
{ age: 0, structure: 'Forest Cave' },
{ age: 0, structure: 'Loom' },
{ age: 0, structure: 'Ore Vein' },
{ age: 0, structure: 'Guard Tower' },
{ age: 0, structure: 'East Trading Post' },
{ age: 1, structure: 'Laboratory' },
{ age: 1, structure: 'Archery Range' },
{ age: 1, structure: 'Dispensary' },
{ age: 1, structure: 'Glassworks' },
{ age: 1, structure: 'Press' },
{ age: 1, structure: 'Glassworks' },
{ age: 2, structure: 'Siege Workshop' },
{ age: 2, structure: 'Philosophers Guild' },
]
}, {
name: 'Player D',
gold: 10,
leftBattles: [-1, -1],
rightBattles: [],
wonder: 'Colossus of Rhodes A',
hand: ['Senate', 'Study', 'Senate', 'Arena'],
structures: [
{ age: 0, structure: 'Excavation' },
{ age: 0, structure: 'Lumber Yard' },
{ age: 0, structure: 'Clay Pit' },
{ age: 0, structure: 'Workshop' },
{ age: 0, structure: 'Tavern' },
{ age: 0, structure: 'West Trading Post' },
{ age: 1, structure: 'Temple' },
{ age: 1, structure: 'Vineyard' },
{ age: 1, structure: 'Stables' },
{ age: 1, structure: 'Press' },
{ age: 1, structure: 'Quarry' },
{ age: 1, structure: 'Brickyard' },
{ age: 2, structure: 'Gardens' },
{ age: 2, structure: 'Circus' },
]
}, {
name: 'Player E',
gold: 0,
leftBattles: [],
rightBattles: [-1, -1],
wonder: 'Great Pyramid of Giza A',
hand: ['Observatory', 'Academy', 'University', 'Lodge'],
structures: [
{ age: 0, structure: 'Apothecary' },
{ age: 0, structure: 'Tavern' },
{ age: 0, structure: 'Altar' },
{ age: 0, structure: 'Theater' },
{ age: 0, structure: 'Marketplace' },
{ age: 0, structure: 'Altar' },
{ age: 1, structure: 'Sawmill' },
{ age: 1, structure: 'Foundry' },
{ age: 1, structure: 'Brickyard' },
{ age: 1, structure: 'Dispensary' },
{ age: 1, structure: 'Training Ground' },
{ age: 1, structure: 'School' },
{ age: 2, structure: 'Siege Workshop' },
{ age: 2, structure: 'Gardens' },
]
},
],
}
export interface BaseAction {
type: string
}
export interface AddPlayerAction extends BaseAction {
type: 'add player'
name: string
}
export function addPlayerAction(name: string): AddPlayerAction {
return {
type: 'add player',
name,
}
}
export interface RemovePlayerAction extends BaseAction {
type: 'remove player'
name: string
}
export function removePlayerAction(name: string): RemovePlayerAction {
return {
type: 'remove player',
name,
}
}
export interface ChooseWonderAction extends BaseAction {
type: 'choose wonder'
player: string
wonder: string
}
export function chooseWonderAction(player: string, wonder: string): ChooseWonderAction {
return {
type: 'choose wonder',
player,
wonder,
}
}
export interface StartGameAction extends BaseAction {
type: 'start game'
shuffle: number[]
}
export function startGameAction(numPlayers: number): StartGameAction {
return {
type: 'start game',
shuffle: shuffle([...Array(numPlayers).keys()]),
}
}
export interface PlanTurnAction extends BaseAction {
type: 'plan turn'
player: string
turnPlan?: TurnPlan
}
export function planTurnAction(player: string, turnPlan?: TurnPlan): PlanTurnAction {
return {
type: 'plan turn',
player,
turnPlan,
}
}
export interface ResetAction extends BaseAction {
type: 'reset'
}
export function resetAction(): ResetAction {
return { type: 'reset' }
}
export interface LoadSampleAction extends BaseAction {
type: 'load sample'
}
export function loadSampleAction(): LoadSampleAction {
return { type: 'load sample' }
}
export type Action = AddPlayerAction | RemovePlayerAction | ChooseWonderAction | StartGameAction
| PlanTurnAction
| ResetAction | LoadSampleAction
function sumCountable(player: Player, countable: Countable): number {
switch (countable) {
case 'wonder stage':
return 0 // TODO
case 'defeat':
return [...player.leftBattles, ...player.rightBattles].filter(b => b === defeatPoints).length
default:
return player.structures.filter(({ structure }) => structures.get(structure)?.type === countable).length
}
}
function sumCountableMap(player: Player, map: Map<Countable, number>): number {
return sum([...map.entries()].map(([countable, number]) => number * sumCountable(player, countable)))
}
function scoreScience(
sciences: Set<Science>[],
counts: Map<Science, number> = new Map([['engineering', 0], ['research', 0], ['education', 0]]),
): number {
if (sciences.length === 0) {
return (scienceSetPoints * (min([...counts.values()]) ?? 0))
+ sum([...counts.values()].map(c => c * c))
} else {
return max([...head(sciences)!.values()].map(science =>
scoreScience(tail(sciences), new Map([...counts, [science, (counts.get(science) ?? 0) + 1]]))
)) ?? 0
}
}
export function countPoints(state: State, playerIdx: number): number {
const player = state.players[playerIdx]
const left = state.players[(playerIdx + 1) % state.players.length]
const right = state.players[(playerIdx - 1 + state.players.length) % state.players.length]
return sum(player.leftBattles) + sum(player.rightBattles)
+ sum(player.structures.map(({ structure }) => {
const card = structures.get(structure)!
return (card.points ?? 0)
+ sumCountableMap(player, card.pointsPer ?? new Map())
+ sumCountableMap(left, card.pointsPerNeighbor ?? new Map())
+ sumCountableMap(right, card.pointsPerNeighbor ?? new Map())
}))
+ Math.floor(player.gold / goldPerPoint)
+ scoreScience(player.structures.flatMap(({ structure }) => {
const sciences = structures.get(structure)?.sciences
return sciences === undefined ? [] : [sciences]
}))
}
function beginAge(state: State): State {
const age = state.age === undefined ? 0 : state.age + 1
const deck = shuffle(buildDeck(age, state.players.length).map(s => s.name)) // BAD
const handSize = Math.ceil(deck.length / state.players.length)
const hands = chunk(deck, handSize)
return {
...state,
age,
turnsRemaining: handSize - 1,
players: state.players.map((p, i) => ({ ...p, hand: hands[i] })),
}
}
function countShields(player: Player): number {
return sum(player.structures.map(({ structure }) => structures.get(structure)?.shields))
}
function battleResult(state: State, playerIdx: number, delta: number): number | undefined {
const playerShields = countShields(state.players[playerIdx])
const otherShields = countShields(state.players[(playerIdx + delta + state.players.length) % state.players.length])
if (playerShields > otherShields) {
return victoryPoints[state.age!]
} else if (playerShields < otherShields) {
return defeatPoints
}
}
function endAge(state: State): State {
return {
...state,
turnsRemaining: undefined,
discard: [...state.discard, ...state.players.flatMap(p => p.hand)],
players: state.players.map((p, i) => {
const left = battleResult(state, i, 1)
const right = battleResult(state, i, -1)
return {
...p,
hand: [],
leftBattles: [...p.leftBattles, ...(left === undefined ? [] : [left])],
rightBattles: [...p.rightBattles, ...(right === undefined ? [] : [right])],
}
}),
}
}
function doTurn(state: State): State {
state = {
...state,
players: state.players.map((player, playerIdx) => {
const turnPlan = player.turnPlan!
const left = state.players[(playerIdx + 1) % state.players.length]
const right = state.players[(playerIdx - 1 + state.players.length) % state.players.length]
const card = structures.get(player.hand[turnPlan.cardIdx])!
return {
...player,
gold: player.gold
+ left.turnPlan!.goldRight + right.turnPlan!.goldLeft
+ (turnPlan.action === 'discard' ? 3 : 0)
+ (turnPlan.action !== 'structure' ? 0 :
(card.gold ?? 0)
+ sumCountableMap(player, card.goldPer ?? new Map())
+ sumCountableMap(left, card.goldPerNeighbor ?? new Map())
+ sumCountableMap(right, card.goldPerNeighbor ?? new Map())
),
hand: [
...player.hand.slice(0, turnPlan.cardIdx),
...player.hand.slice(turnPlan.cardIdx + 1)
],
structures: [
...player.structures,
...(turnPlan.action !== 'structure' ? [] : [
{ age: state.age!, structure: player.hand[turnPlan.cardIdx] }
]),
]
}
}),
turnsRemaining: state.turnsRemaining! - 1,
}
if (state.turnsRemaining === 0) {
state = endAge(state)
if (state.age === numAges - 1) {
return {
...state,
stage: 'finished',
age: undefined,
}
} else {
return beginAge(state)
}
} else {
const delta = state.age! % 2 === 1 ? -1 : 1
return {
...state,
players: state.players.map((p, i) => ({
...p,
hand: state.players[(i + delta + state.players.length) % state.players.length].hand,
})),
}
}
}
const reducers: { [type: string]: (state: State, action: any) => State } = {
'add player': (state, action: AddPlayerAction) => {
if (state.stage !== 'starting' || state.players.some(p => p.name === action.name)) { return state }
return {
...state,
players: [...state.players, {
name: action.name,
gold: startingGold,
structures: [],
hand: [],
leftBattles: [],
rightBattles: [],
}],
}
},
'remove player': (state, action: RemovePlayerAction) => {
if (state.stage !== 'starting') {
return state
}
return {
...state,
players: state.players.filter(p => p.name !== action.name),
}
},
'choose wonder': (state, action: ChooseWonderAction) => {
if (state.stage !== 'starting' || state.players.some(p => p.wonder === action.wonder)) {
return state
}
return {
...state,
players: state.players.map(p => p.name === action.player ? { ...p, wonder: action.wonder } : p),
}
},
'start game': (state, action: StartGameAction) => {
if (state.stage !== 'starting' || state.players.length < minPlayers || state.players.length > maxPlayers || state.players.some(p => p.wonder === undefined)) {
return state
}
return beginAge({
...state,
stage: 'play',
players: action.shuffle.map(i => state.players[i]),
})
},
'plan turn': (state, action: PlanTurnAction) => {
if (state.stage !== 'play') {
return state
}
const newState = {
...state,
players: state.players.map(
p => p.name !== action.player ? p : { ...p, turnPlan: action.turnPlan }
),
}
return newState.players.every(p => p.turnPlan !== undefined) ? doTurn(newState) : newState
},
'reset': () => initial,
'load sample': () => sampleState,
}
export function reducer(state: State, action: Action): State {
return reducers[action.type](state, action)
}

View File

@ -1,166 +0,0 @@
import chunk from "lodash/chunk"
import shuffle from "lodash/shuffle"
import sum from 'lodash/sum'
import { Action, AddPlayerAction, ChooseWonderAction, PlanTurnAction, RemovePlayerAction, StartGameAction } from "./action"
import { defeatPoints, initial, maxPlayers, minPlayers, numAges, Player, sampleState, startingGold, State, victoryPoints } from "./basic"
import { structures } from "./structure"
import { buildDeck, sumCountableMap } from "./util"
function beginAge(state: State): State {
const age = state.age === undefined ? 0 : state.age + 1
const deck = shuffle(buildDeck(age, state.players.length).map(s => s.name)) // BAD
const handSize = Math.ceil(deck.length / state.players.length)
const hands = chunk(deck, handSize)
return {
...state,
age,
turnsRemaining: handSize - 1,
players: state.players.map((p, i) => ({ ...p, hand: hands[i] })),
}
}
function countShields(player: Player): number {
return sum(player.structures.map(({ structure }) => structures.get(structure)?.shields))
}
function battleResult(state: State, playerIdx: number, delta: number): number | undefined {
const playerShields = countShields(state.players[playerIdx])
const otherShields = countShields(state.players[(playerIdx + delta + state.players.length) % state.players.length])
if (playerShields > otherShields) {
return victoryPoints[state.age!]
} else if (playerShields < otherShields) {
return defeatPoints
}
}
function endAge(state: State): State {
return {
...state,
turnsRemaining: undefined,
discard: [...state.discard, ...state.players.flatMap(p => p.hand)],
players: state.players.map((p, i) => {
const left = battleResult(state, i, 1)
const right = battleResult(state, i, -1)
return {
...p,
hand: [],
leftBattles: [...p.leftBattles, ...(left === undefined ? [] : [left])],
rightBattles: [...p.rightBattles, ...(right === undefined ? [] : [right])],
}
}),
}
}
function doTurn(state: State): State {
state = {
...state,
players: state.players.map((player, playerIdx) => {
const turnPlan = player.turnPlan!
const left = state.players[(playerIdx + 1) % state.players.length]
const right = state.players[(playerIdx - 1 + state.players.length) % state.players.length]
const card = structures.get(player.hand[turnPlan.cardIdx])!
return {
...player,
gold: player.gold
+ left.turnPlan!.goldRight + right.turnPlan!.goldLeft
+ (turnPlan.action === 'discard' ? 3 : 0)
+ (turnPlan.action !== 'structure' ? 0 :
(card.gold ?? 0)
+ sumCountableMap(player, card.goldPer ?? new Map())
+ sumCountableMap(left, card.goldPerNeighbor ?? new Map())
+ sumCountableMap(right, card.goldPerNeighbor ?? new Map())
),
hand: [
...player.hand.slice(0, turnPlan.cardIdx),
...player.hand.slice(turnPlan.cardIdx + 1)
],
structures: [
...player.structures,
...(turnPlan.action !== 'structure' ? [] : [
{ age: state.age!, structure: player.hand[turnPlan.cardIdx] }
]),
]
}
}),
turnsRemaining: state.turnsRemaining! - 1,
}
if (state.turnsRemaining === 0) {
state = endAge(state)
if (state.age === numAges - 1) {
return {
...state,
stage: 'finished',
age: undefined,
}
} else {
return beginAge(state)
}
} else {
const delta = state.age! % 2 === 1 ? -1 : 1
return {
...state,
players: state.players.map((p, i) => ({
...p,
hand: state.players[(i + delta + state.players.length) % state.players.length].hand,
})),
}
}
}
const reducers: { [type: string]: (state: State, action: any) => State } = {
'add player': (state, action: AddPlayerAction) => {
if (state.stage !== 'starting' || state.players.some(p => p.name === action.name)) { return state }
return {
...state,
players: [...state.players, {
name: action.name,
gold: startingGold,
structures: [],
hand: [],
leftBattles: [],
rightBattles: [],
}],
}
},
'remove player': (state, action: RemovePlayerAction) => {
if (state.stage !== 'starting') {
return state
}
return {
...state,
players: state.players.filter(p => p.name !== action.name),
}
},
'choose wonder': (state, action: ChooseWonderAction) => {
if (state.stage !== 'starting' || state.players.some(p => p.wonder === action.wonder)) {
return state
}
return {
...state,
players: state.players.map(p => p.name === action.player ? { ...p, wonder: action.wonder } : p),
}
},
'start game': (state, action: StartGameAction) => {
if (state.stage !== 'starting' || state.players.length < minPlayers || state.players.length > maxPlayers || state.players.some(p => p.wonder === undefined)) {
return state
}
return beginAge({
...state,
stage: 'play',
players: action.shuffle.map(i => state.players[i]),
})
},
'plan turn': (state, action: PlanTurnAction) => {
if (state.stage !== 'play') {
return state
}
const newState = {
...state,
players: state.players.map(
p => p.name !== action.player ? p : { ...p, turnPlan: action.turnPlan }
),
}
return newState.players.every(p => p.turnPlan !== undefined) ? doTurn(newState) : newState
},
'reset': () => initial,
'load sample': () => sampleState,
}
export function reducer(state: State, action: Action): State {
return reducers[action.type](state, action)
}

View File

@ -1,136 +0,0 @@
import { Resource, Science } from "./basic"
export type StructureType = 'commerce' | 'culture' | 'science' | 'basic industry' | 'advanced industry' | 'guild' | 'military'
export const structureTypes: StructureType[] = ['commerce', 'culture', 'science', 'basic industry', 'advanced industry', 'guild', 'military']
export type Countable = StructureType | 'wonder stage' | 'defeat'
export interface Project {
cost?: Map<Resource | 'gold', number>
gold?: number
goldPer?: Map<Countable, number>
goldPerNeighbor?: Map<Countable, number>
points?: number
pointsPer?: Map<Countable, number>
pointsPerNeighbor?: Map<Countable, number>
sciences?: Set<Science>
resources?: Map<Resource, number>
shields?: number
discountTrade?: {
resourceType: 'basic' | 'advanced'
direction: ('left' | 'right')[]
amount: number
}[]
playBothOnLastTurn?: boolean
buildDiscardedNow?: boolean
copyNeighboringGuild?: boolean
ignoreCostOncePerAge?: boolean
}
export interface Structure extends Project {
type: StructureType
name: string
appears?: (undefined | (3 | 4 | 5 | 6 | 7)[])[]
freeWith?: string[]
paysFor?: string[]
}
export const structuresByAge: Structure[][] = [
[
{ type: 'advanced industry', name: 'Glassworks', appears: [[3, 6], [3, 5]], resources: new Map([['glass', 1]]) },
{ type: 'advanced industry', name: 'Loom', appears: [[3, 6], [3, 5]], resources: new Map([['cloth', 1]]) },
{ type: 'advanced industry', name: 'Press', appears: [[3, 6], [3, 5]], resources: new Map([['paper', 1]]) },
{ type: 'basic industry', name: 'Clay Pit', appears: [[3]], cost: new Map([['gold', 1]]), resources: new Map([['brick', 1], ['ore', 1]]) },
{ type: 'basic industry', name: 'Clay Pool', appears: [[3, 5]], resources: new Map([['brick', 1]]) },
{ type: 'basic industry', name: 'Excavation', appears: [[4]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 1], ['brick', 1]]) },
{ type: 'basic industry', name: 'Forest Cave', appears: [[5]], cost: new Map([['gold', 1]]), resources: new Map([['wood', 1], ['ore', 1]]) },
{ type: 'basic industry', name: 'Lumber Yard', appears: [[3, 4]], resources: new Map([['wood', 1]]) },
{ type: 'basic industry', name: 'Mine', appears: [[6]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 1], ['ore', 1]]) },
{ type: 'basic industry', name: 'Ore Vein', appears: [[3, 4]], resources: new Map([['ore', 1]]) },
{ type: 'basic industry', name: 'Stone Pit', appears: [[3, 5]], resources: new Map([['stone', 1]]) },
{ type: 'basic industry', name: 'Timber Yard', appears: [[3]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 1], ['wood', 1]]) },
{ type: 'basic industry', name: 'Tree Farm', appears: [[6]], cost: new Map([['gold', 1]]), resources: new Map([['wood', 1], ['brick', 1]]) },
{ type: 'commerce', name: 'East Trading Post', appears: [[3, 7]], discountTrade: [{ resourceType: 'basic', direction: ['right'], amount: 1 }] },
{ type: 'commerce', name: 'Marketplace', appears: [[3, 6]], discountTrade: [{ resourceType: 'advanced', direction: ['left', 'right'], amount: 1 }] },
{ type: 'commerce', name: 'Tavern', appears: [[4, 5, 7]], gold: 5 },
{ type: 'commerce', name: 'West Trading Post', appears: [[3, 7]], discountTrade: [{ resourceType: 'basic', direction: ['left'], amount: 1 }] },
{ type: 'culture', name: 'Altar', appears: [[3, 5]], points: 2 },
{ type: 'culture', name: 'Baths', appears: [[3, 7]], cost: new Map([['stone', 1]]), points: 3 },
{ type: 'culture', name: 'Pawnshop', appears: [[4, 7]], points: 3 },
{ type: 'culture', name: 'Theater', appears: [[3, 6]], points: 2 },
{ type: 'military', name: 'Barracks', appears: [[3, 5]], cost: new Map([['ore', 1]]), shields: 1 },
{ type: 'military', name: 'Guard Tower', appears: [[3, 4]], cost: new Map([['brick', 1]]), shields: 1 },
{ type: 'military', name: 'Stockade', appears: [[3, 7]], cost: new Map([['wood', 1]]), shields: 1 },
{ type: 'science', name: 'Apothecary', appears: [[3, 5]], cost: new Map([['cloth', 1]]), sciences: new Set(['research']) },
{ type: 'science', name: 'Scriptorium', appears: [[3, 4]], cost: new Map([['glass', 1]]), sciences: new Set(['education']) },
{ type: 'science', name: 'Workshop', appears: [[3, 7]], cost: new Map([['glass', 1]]), sciences: new Set(['engineering']) },
], [
{ type: 'basic industry', name: 'Brickyard', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['brick', 2]]) },
{ type: 'basic industry', name: 'Foundry', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['ore', 2]]) },
{ type: 'basic industry', name: 'Sawmill', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['wood', 2]]) },
{ type: 'basic industry', name: 'Quarry', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 2]]) },
{ type: 'commerce', name: 'Bazar', appears: [, [4, 7]], goldPer: new Map([['advanced industry', 2]]), goldPerNeighbor: new Map([['advanced industry', 2]]) },
{ type: 'commerce', name: 'Caravansery', appears: [, [3, 5, 6]], cost: new Map([['wood', 2]]), freeWith: ['Marketplace'], resources: new Map([['wood', 1], ['stone', 1], ['ore', 1], ['brick', 1]]) },
{ type: 'commerce', name: 'Forum', appears: [, [3, 6, 7]], cost: new Map([['brick', 2]]), freeWith: ['East Trading Post', 'West Trading Post'], resources: new Map([['glass', 1], ['cloth', 1], ['paper', 1]]) },
{ type: 'commerce', name: 'Vineyard', appears: [, [3, 6]], goldPer: new Map([['basic industry', 1]]), goldPerNeighbor: new Map([['basic industry', 1]]) },
{ type: 'culture', name: 'Aqueduct', appears: [, [3, 7]], cost: new Map([['stone', 3]]), freeWith: ['Baths'], points: 5 },
{ type: 'culture', name: 'Courthouse', appears: [, [3, 5]], cost: new Map([['brick', 2], ['cloth', 1]]), freeWith: ['Scriptorium'], points: 4 },
{ type: 'culture', name: 'Statue', appears: [, [3, 7]], cost: new Map([['ore', 2], ['wood', 1]]), freeWith: ['Theater'], points: 4 },
{ type: 'culture', name: 'Temple', appears: [, [3, 6]], cost: new Map([['brick', 1], ['wood', 1], ['glass', 1]]), freeWith: ['Altar'], points: 3 },
{ type: 'military', name: 'Archery Range', appears: [, [3, 6]], cost: new Map([['wood', 2], ['ore', 1]]), freeWith: ['Workshop'], shields: 2 },
{ type: 'military', name: 'Stables', appears: [, [3, 5]], cost: new Map([['wood', 1], ['ore', 1], ['brick', 1]]), freeWith: ['Apothecary'], shields: 2 },
{ type: 'military', name: 'Training Ground', appears: [, [4, 6, 7]], cost: new Map([['ore', 2], ['wood', 1]]), shields: 2 },
{ type: 'military', name: 'Walls', appears: [, [3, 7]], cost: new Map([['stone', 3]]), shields: 2 },
{ type: 'science', name: 'Dispensary', appears: [, [3, 4]], cost: new Map([['glass', 1], ['ore', 2]]), freeWith: ['Apothecary'], sciences: new Set(['research']) },
{ type: 'science', name: 'Laboratory', appears: [, [3, 5]], cost: new Map([['paper', 1], ['brick', 2]]), freeWith: ['Workshop'], sciences: new Set(['engineering']) },
{ type: 'science', name: 'Library', appears: [, [3, 6]], cost: new Map([['cloth', 1], ['stone', 2]]), freeWith: ['Scriptorium'], sciences: new Set(['education']) },
{ type: 'science', name: 'School', appears: [, [3, 7]], cost: new Map([['paper', 1], ['wood', 1]]), sciences: new Set(['education']) },
], [
{ type: 'commerce', name: 'Arena', appears: [, , [3, 3, 5, 7]], cost: new Map([['stone', 2], ['ore', 1]]), freeWith: ['Dispensary'], goldPer: new Map([['wonder stage', 3]]), pointsPer: new Map([['wonder stage', 1]]) },
{ type: 'commerce', name: 'Chamber of Commerce', appears: [, , [4, 6]], cost: new Map([['brick', 2], ['paper', 1]]), goldPer: new Map([['advanced industry', 2]]), pointsPer: new Map([['advanced industry', 2]]) },
{ type: 'commerce', name: 'Haven', appears: [, , [4]], cost: new Map([['wood', 1], ['ore', 1], ['paper', 1]]), goldPer: new Map([['basic industry', 1]]), pointsPer: new Map([['basic industry', 1]]) },
{ type: 'commerce', name: 'Lighthouse', appears: [, , [3, 6]], cost: new Map([['stone', 1], ['glass', 1]]), freeWith: ['Caravansery'], goldPer: new Map([['commerce', 1]]), pointsPer: new Map([['commerce', 1]]) },
{ type: 'culture', name: 'Gardens', appears: [, , [3, 4]], cost: new Map([['brick', 2], ['wood', 1]]), points: 5 },
{ type: 'culture', name: 'Palace', appears: [, , [3, 7]], cost: new Map([['wood', 1], ['brick', 1], ['stone', 1], ['ore', 1], ['glass', 1], ['cloth', 1], ['paper', 1]]), points: 8 },
{ type: 'culture', name: 'Pantheon', appears: [, , [3, 6]], cost: new Map([['brick', 2], ['ore', 1], ['glass', 1], ['paper', 1], ['cloth', 1]]), freeWith: ['Temple'], points: 7 },
{ type: 'culture', name: 'Senate', appears: [, , [3, 5]], cost: new Map([['wood', 2], ['ore', 1], ['stone', 1]]), freeWith: ['Library'], points: 6 },
{ type: 'culture', name: 'Town Hall', appears: [, , [3, 5, 6]], cost: new Map([['stone', 2], ['ore', 1], ['glass', 1]]), points: 6 },
{ type: 'military', name: 'Arsenal', appears: [, , [3, 4, 7]], cost: new Map([['wood', 2], ['ore', 1], ['cloth', 1]]), shields: 3 },
{ type: 'military', name: 'Circus', appears: [, , [4, 5, 6]], cost: new Map([['stone', 3], ['ore', 1]]), freeWith: ['Training Ground'], shields: 3 },
{ type: 'military', name: 'Fortifications', appears: [, , [3, 7]], cost: new Map([['ore', 3], ['stone', 1]]), freeWith: ['Walls'], shields: 3 },
{ type: 'military', name: 'Siege Workshop', appears: [, , [3, 5]], cost: new Map([['brick', 3], ['wood', 1]]), freeWith: ['Laboratory'], shields: 3 },
{ type: 'science', name: 'Academy', appears: [, , [3, 7]], cost: new Map([['stone', 3], ['glass', 1]]), freeWith: ['School'], sciences: new Set(['research']) },
{ type: 'science', name: 'Lodge', appears: [, , [3, 6]], cost: new Map([['brick', 2], ['paper', 1], ['cloth', 1]]), freeWith: ['Dispensary'], sciences: new Set(['research']) },
{ type: 'science', name: 'Observatory', appears: [, , [3, 7]], cost: new Map([['ore', 2], ['glass', 1], ['cloth', 1]]), freeWith: ['Laboratory'], sciences: new Set(['engineering']) },
{ type: 'science', name: 'Study', appears: [, , [3, 5]], cost: new Map([['wood', 1], ['paper', 1], ['cloth', 1]]), freeWith: ['School'], sciences: new Set(['engineering']) },
{ type: 'science', name: 'University', appears: [, , [3, 4]], cost: new Map([['wood', 2], ['paper', 1], ['glass', 1]]), freeWith: ['Library'], sciences: new Set(['education']) },
],
]
export const structuresGuilds: Structure[] = [
{ type: 'guild', name: 'Builders Guild', cost: new Map([['stone', 2], ['brick', 2], ['glass', 1]]), pointsPer: new Map([['wonder stage', 1]]), pointsPerNeighbor: new Map([['wonder stage', 1]]) },
{ type: 'guild', name: 'Craftsmens Guild', cost: new Map([['ore', 2], ['stone', 2]]), pointsPerNeighbor: new Map([['advanced industry', 2]]) },
{ type: 'guild', name: 'Magistrates Guild', cost: new Map([['wood', 3], ['stone', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['culture', 1]]) },
{ type: 'guild', name: 'Philosophers Guild', cost: new Map([['brick', 3], ['paper', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['science', 1]]) },
{ type: 'guild', name: 'Scientists Guild', cost: new Map([['ore', 2], ['wood', 2], ['paper', 1]]), sciences: new Set(['engineering', 'research', 'education']) },
{ type: 'guild', name: 'Shipowners Guild', cost: new Map([['wood', 3], ['glass', 1], ['paper', 1]]), pointsPer: new Map([['basic industry', 1], ['advanced industry', 1], ['guild', 1]]) },
{ type: 'guild', name: 'Spies Guild', cost: new Map([['brick', 3], ['glass', 1]]), pointsPerNeighbor: new Map([['military', 1]]) },
{ type: 'guild', name: 'Strategists Guild', cost: new Map([['ore', 2], ['stone', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['defeat', 1]]) },
{ type: 'guild', name: 'Traders Guild', cost: new Map([['glass', 1], ['paper', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['commerce', 1]]) },
{ type: 'guild', name: 'Workers Guild', cost: new Map([['ore', 2], ['brick', 1], ['stone', 1], ['wood', 1]]), pointsPerNeighbor: new Map([['basic industry', 1]]) },
]
export const structures = new Map<string, Structure>([...structuresByAge, structuresGuilds].flat().map(c => ([c.name, c])))
// Initialize 'paysFor' properties
for (const structure of structures.values()) {
for (const precursorName of structure.freeWith ?? []) {
const precursor = structures.get(precursorName)!
if (precursor.paysFor === undefined) {
precursor.paysFor = [structure.name]
} else {
precursor.paysFor.push(structure.name)
}
}
}

View File

@ -1,97 +0,0 @@
import { defeatPoints, goldPerPoint, maxPlayers, minPlayers, numAges, Player, Science, scienceSetPoints, State } from "./basic"
import isInteger from 'lodash/isInteger'
import sampleSize from 'lodash/sampleSize'
import compact from 'lodash/compact'
import sum from 'lodash/sum'
import min from 'lodash/min'
import max from 'lodash/max'
import head from 'lodash/head'
import tail from 'lodash/tail'
import { Countable, Structure, structures, structuresGuilds } from "./structure"
export function buildDeck(age: number, numPlayers: number) {
if (age < 0 || age >= numAges || !isInteger(age)) {
throw new Error(`Unsupported age ${age}`)
}
if (numPlayers < minPlayers || numPlayers > maxPlayers || !isInteger(numPlayers)) {
throw new Error(`Unsupported player count ${numPlayers}`)
}
const cards: Structure[] = [...structures.values()].flatMap(structure =>
structure.appears?.[age]
?.filter(minPlayers => numPlayers <= minPlayers)
?.map(() => structure)
?? []
)
if (age === numAges - 1) {
cards.push(...sampleSize(structuresGuilds, numPlayers + 2)) // BAD
}
return cards
}
export function sumMaps<V>(ms?: (Map<V, number> | undefined)[]) {
const sum = new Map<V, number>()
for (const m of ms ?? []) {
for (const [k, v] of m ?? []) {
sum.set(k, (sum.get(k) ?? 0) + v)
}
}
return sum
}
export function playerStats(p: Player) {
const structObjs = compact(p.structures.map(s => ({ ...s, structure: structures.get(s.structure)! })))
return {
structObjs,
resources: sumMaps(structObjs.map(s => s.structure?.resources)),
shields: sum(structObjs.map(s => s.structure?.shields)) ?? 0,
sciences: compact(structObjs.map(s => s.structure?.sciences)),
}
}
export function sumCountable(player: Player, countable: Countable): number {
switch (countable) {
case 'wonder stage':
return 0 // TODO
case 'defeat':
return [...player.leftBattles, ...player.rightBattles].filter(b => b === defeatPoints).length
default:
return player.structures.filter(({ structure }) => structures.get(structure)?.type === countable).length
}
}
export function sumCountableMap(player: Player, map: Map<Countable, number>): number {
return sum([...map.entries()].map(([countable, number]) => number * sumCountable(player, countable)))
}
export function scoreScience(
sciences: Set<Science>[],
counts: Map<Science, number> = new Map([['engineering', 0], ['research', 0], ['education', 0]]),
): number {
if (sciences.length === 0) {
return (scienceSetPoints * (min([...counts.values()]) ?? 0))
+ sum([...counts.values()].map(c => c * c))
} else {
return max([...head(sciences)!.values()].map(science =>
scoreScience(tail(sciences), new Map([...counts, [science, (counts.get(science) ?? 0) + 1]]))
)) ?? 0
}
}
export function countPoints(state: State, playerIdx: number): number {
const player = state.players[playerIdx]
const left = state.players[(playerIdx + 1) % state.players.length]
const right = state.players[(playerIdx - 1 + state.players.length) % state.players.length]
return sum(player.leftBattles) + sum(player.rightBattles)
+ sum(player.structures.map(({ structure }) => {
const card = structures.get(structure)!
return (card.points ?? 0)
+ sumCountableMap(player, card.pointsPer ?? new Map())
+ sumCountableMap(left, card.pointsPerNeighbor ?? new Map())
+ sumCountableMap(right, card.pointsPerNeighbor ?? new Map())
}))
+ Math.floor(player.gold / goldPerPoint)
+ scoreScience(player.structures.flatMap(({ structure }) => {
const sciences = structures.get(structure)?.sciences
return sciences === undefined ? [] : [sciences]
}))
}

View File

@ -1,125 +0,0 @@
import { Resource } from "./basic"
import { Project } from "./structure"
export interface Wonder {
name: string
innateResource: Resource
stages: Project[]
}
const wonderList: Wonder[] = [
{
name: 'Great Pyramid of Giza A',
innateResource: 'stone',
stages: [
{ cost: new Map([['stone', 2]]), points: 3 },
{ cost: new Map([['wood', 3]]), points: 5 },
{ cost: new Map([['stone', 4]]), points: 7 },
],
}, {
name: 'Great Pyramid of Giza B',
innateResource: 'stone',
stages: [
{ cost: new Map([['wood', 2]]), points: 3 },
{ cost: new Map([['stone', 3]]), points: 5 },
{ cost: new Map([['brick', 3]]), points: 5 },
{ cost: new Map([['paper', 1], ['stone', 4]]), points: 7 },
],
}, {
name: 'Hanging Gardens of Babylon A',
innateResource: 'brick',
stages: [
{ cost: new Map([['brick', 2]]), points: 3 },
{ cost: new Map([['wood', 3]]), sciences: new Set(['engineering', 'research', 'education']) },
{ cost: new Map([['brick', 4]]), points: 7 },
],
}, {
name: 'Hanging Gardens of Babylon B',
innateResource: 'brick',
stages: [
{ cost: new Map([['cloth', 1], ['brick', 1]]), points: 3 },
{ cost: new Map([['glass', 1], ['wood', 2]]), playBothOnLastTurn: true },
{ cost: new Map([['paper', 1], ['brick', 3]]), sciences: new Set(['engineering', 'research', 'education']) },
],
}, {
name: 'Temple of Artemis at Ephesus A',
innateResource: 'paper',
stages: [
{ cost: new Map([['stone', 2]]), points: 3 },
{ cost: new Map([['wood', 2]]), gold: 9 },
{ cost: new Map([['paper', 2]]), points: 7 },
],
}, {
name: 'Temple of Artemis at Ephesus B',
innateResource: 'paper',
stages: [
{ cost: new Map([['stone', 2]]), points: 2, gold: 4 },
{ cost: new Map([['wood', 2]]), points: 3, gold: 4 },
{ cost: new Map([['paper', 1], ['cloth', 1], ['glass', 1]]), points: 5, gold: 4 },
],
}, {
name: 'Statue of Zeus at Olympia A',
innateResource: 'wood',
stages: [
{ cost: new Map([['wood', 2]]), points: 3 },
{ cost: new Map([['stone', 2]]), ignoreCostOncePerAge: true },
{ cost: new Map([['ore', 2]]), points: 7 },
],
}, {
name: 'Statue of Zeus at Olympia B',
innateResource: 'wood',
stages: [
{ cost: new Map([['wood', 2]]), discountTrade: [{ resourceType: 'basic', direction: ['left', 'right'], amount: 1 }] },
{ cost: new Map([['stone', 2]]), points: 5 },
{ cost: new Map([['cloth', 1], ['ore', 2]]), copyNeighboringGuild: true },
],
}, {
name: 'Mausoleum at Halicarnassus A',
innateResource: 'cloth',
stages: [
{ cost: new Map([['brick', 2]]), points: 3 },
{ cost: new Map([['ore', 3]]), buildDiscardedNow: true },
{ cost: new Map([['cloth', 2]]), points: 7 },
],
}, {
name: 'Mausoleum at Halicarnassus B',
innateResource: 'cloth',
stages: [
{ cost: new Map([['ore', 2]]), points: 2, buildDiscardedNow: true },
{ cost: new Map([['brick', 3]]), points: 1, buildDiscardedNow: true },
{ cost: new Map([['paper', 1], ['cloth', 1], ['glass', 1]]), buildDiscardedNow: true },
],
}, {
name: 'Colossus of Rhodes A',
innateResource: 'ore',
stages: [
{ cost: new Map([['wood', 2]]), points: 3 },
{ cost: new Map([['brick', 3]]), shields: 2 },
{ cost: new Map([['ore', 4]]), points: 7 },
],
}, {
name: 'Colossus of Rhodes B',
innateResource: 'ore',
stages: [
{ cost: new Map([['stone', 3]]), shields: 1, points: 3, gold: 3 },
{ cost: new Map([['ore', 4]]), shields: 1, points: 4, gold: 4 },
],
}, {
name: 'Lighthouse of Alexandria A',
innateResource: 'glass',
stages: [
{ cost: new Map([['stone', 2]]), points: 3 },
{ cost: new Map([['ore', 2]]), resources: new Map([['brick', 1], ['ore', 1], ['wood', 1], ['stone', 1]]) },
{ cost: new Map([['glass', 2]]), points: 7 },
],
}, {
name: 'Lighthouse of Alexandria B',
innateResource: 'glass',
stages: [
{ cost: new Map([['brick', 2]]), resources: new Map([['brick', 1], ['ore', 1], ['wood', 1], ['stone', 1]]) },
{ cost: new Map([['wood', 2]]), resources: new Map([['glass', 1], ['cloth', 1], ['paper', 1]]) },
{ cost: new Map([['stone', 3]]), points: 7 },
],
},
]
export const wonders = new Map(wonderList.map(w => [w.name, w]))

View File

@ -1,23 +0,0 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": [
"standard"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"comma-dangle": ["error", "only-multiline"],
"padded-blocks": "warn",
"no-multiple-empty-lines": "warn",
"space-before-function-paren": "off"
}
}

View File

@ -1,21 +0,0 @@
FROM node:16-alpine
RUN apk add --no-cache tini
RUN yarn global add lerna
WORKDIR /opt/wonders-server
COPY packages/ketchup-common/package.json packages/ketchup-common/
COPY packages/ketchup-server/package.json packages/ketchup-server/
COPY packages/wonders-common/package.json packages/wonders-common/
COPY packages/wonders-server/package.json packages/wonders-server/
COPY package.json yarn.lock lerna.json ./
RUN lerna bootstrap
COPY . ./
RUN lerna run build-lib
RUN lerna run build
ENTRYPOINT ["tini", "--"]
CMD ["node", "packages/wonders-server"]
EXPOSE 80

View File

@ -1,36 +0,0 @@
{
"name": "wonders-server",
"version": "0.1.3",
"description": "WebSocket-based rollback state synchronization server.",
"main": "dist/index.js",
"license": "MIT",
"private": true,
"dependencies": {
"ketchup-server": "^0.1.1",
"lodash": "^4.17.20",
"ts-node": "^10.4.0",
"wonders-common": "^0.1.1",
"ws": "^8.2.3"
},
"devDependencies": {
"@tsconfig/node14": "^1.0.0",
"@types/jest": "^27.0.2",
"@types/lodash": "^4.14.176",
"@types/node": "^16.11.4",
"@types/ws": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^5.1.0",
"@typescript-eslint/parser": "^5.1.0",
"eslint": "^8.1.0",
"jest": "^27.3.1",
"ts-jest": "^27.0.7",
"typescript": "^4.1.3"
},
"optionalDependencies": {
"bufferutil": "^4.0.3",
"utf-8-validate": "^5.0.4"
},
"scripts": {
"build": "tsc --declaration",
"start": "node dist"
}
}

View File

@ -1,22 +0,0 @@
import WebSocket from 'ws'
import KetchupServer from 'ketchup-server'
import { initial, reducer } from 'wonders-common'
const port = parseInt(process.env.PORT ?? '80')
const wss = new WebSocket.Server({ port })
const ks = new KetchupServer(initial, reducer)
wss.on('connection', ws => {
ks.addRemoteClient(ws)
})
console.log(`Listening on ${port}`)
function shutdown() {
console.log('Shutting down')
wss.close()
}
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)

View File

@ -1,7 +0,0 @@
{
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
}
}

14298
yarn.lock Normal file

File diff suppressed because it is too large Load Diff