The press
Three things a power can offer, because these are the three that change a turn: support for a named move on a named turn, a demilitarised province, and peace. All concrete, all about a particular turn, all checkable by arithmetic against the orders actually submitted. A game where the machine decides by feel whether you kept your word is a game where being betrayed feels arbitrary rather than infuriating. Deals are judged on the orders given, never on how they turned out. A support that was ordered and cut was still given; a move that bounced was still attempted. Blaming a power for a bounce would make every alliance a lottery and every honest ally look like a liar. Contradictory promises are the mechanic rather than something to prevent. Promising a province to one power and supporting another into it is a double deal, and conflicts() exists so that a power can be shown it has promised two things it cannot both do -- at the moment of agreeing, deliberately. Peace covers the centres a power owns as well as the provinces it occupies. Walking into an undefended centre and calling it peace because nobody was home is the first thing anybody tries. A test asserted the wrong thing here and the comment above the code was promising more than the code did; the code now does what it said. Trust is directional and fades. A stranger starts above halfway, because a game where every stranger is a proven liar never gets started, and an old betrayal loses weight without leaving the ledger, so a broken alliance can re-form out of need.
This commit is contained in:
@@ -0,0 +1,232 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { boardFrom, type Order, type Unit } from './orders'
|
||||||
|
import type { Power } from './map'
|
||||||
|
import {
|
||||||
|
conflicts,
|
||||||
|
emptyLedger,
|
||||||
|
judge,
|
||||||
|
look,
|
||||||
|
remember,
|
||||||
|
trust,
|
||||||
|
type Agreement,
|
||||||
|
type Deal,
|
||||||
|
} from './press'
|
||||||
|
|
||||||
|
const A = (power: Power, at: string): Unit => ({ power, type: 'army', at })
|
||||||
|
const mv = (at: string, to: string): Order => ({ type: 'move', at, to })
|
||||||
|
const sup = (at: string, from: string, to: string): Order => ({ type: 'support', at, from, to })
|
||||||
|
const hold = (at: string): Order => ({ type: 'hold', at })
|
||||||
|
|
||||||
|
const deal = (from: Power, to: Power, d: Deal, turn = 1): Agreement => ({
|
||||||
|
id: `${from}-${to}-${turn}`,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
turn,
|
||||||
|
deal: d,
|
||||||
|
})
|
||||||
|
|
||||||
|
const board = boardFrom([A('austria', 'vie'), A('russia', 'war'), A('italy', 'ven')])
|
||||||
|
|
||||||
|
const orders = (o: Partial<Record<Power, Order[]>>): Map<Power, Order[]> =>
|
||||||
|
new Map(Object.entries(o) as [Power, Order[]][])
|
||||||
|
|
||||||
|
describe('a promise of support', () => {
|
||||||
|
const d = deal('austria', 'russia', {
|
||||||
|
kind: 'support',
|
||||||
|
mover: 'austria',
|
||||||
|
helper: 'russia',
|
||||||
|
from: 'vie',
|
||||||
|
to: 'gal',
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is kept by both when both do as they said', () => {
|
||||||
|
const v = judge(d, orders({ austria: [mv('vie', 'gal')], russia: [sup('war', 'vie', 'gal')] }), board)
|
||||||
|
expect(v.every((x) => x.kept)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('names the one who did not, and only that one', () => {
|
||||||
|
const v = judge(d, orders({ austria: [mv('vie', 'gal')], russia: [hold('war')] }), board)
|
||||||
|
const broke = v.filter((x) => !x.kept)
|
||||||
|
expect(broke).toHaveLength(1)
|
||||||
|
expect(broke[0]!.power).toBe('russia')
|
||||||
|
expect(broke[0]!.why).toContain('did not order the support')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('binds the asker too, who has to actually make the move', () => {
|
||||||
|
const v = judge(d, orders({ austria: [hold('vie')], russia: [sup('war', 'vie', 'gal')] }), board)
|
||||||
|
expect(v.find((x) => x.power === 'austria')!.kept).toBe(false)
|
||||||
|
expect(v.find((x) => x.power === 'russia')!.kept).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is judged on the order given, not on how it turned out', () => {
|
||||||
|
/*
|
||||||
|
* The support was ordered and would have been cut, or the move bounced.
|
||||||
|
* Neither is a lie. Blaming a power for a bounce would make every
|
||||||
|
* alliance a lottery and every honest ally look like a liar.
|
||||||
|
*/
|
||||||
|
const v = judge(d, orders({ austria: [mv('vie', 'gal')], russia: [sup('war', 'vie', 'gal')] }), board)
|
||||||
|
expect(v.every((x) => x.kept)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('a demilitarised province', () => {
|
||||||
|
const d = deal('austria', 'russia', { kind: 'dmz', province: 'gal' })
|
||||||
|
|
||||||
|
it('holds when both stay out', () => {
|
||||||
|
const v = judge(d, orders({ austria: [hold('vie')], russia: [hold('war')] }), board)
|
||||||
|
expect(v.every((x) => x.kept)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is broken by walking in', () => {
|
||||||
|
const v = judge(d, orders({ austria: [hold('vie')], russia: [mv('war', 'gal')] }), board)
|
||||||
|
expect(v.find((x) => x.power === 'russia')!.kept).toBe(false)
|
||||||
|
expect(v.find((x) => x.power === 'austria')!.kept).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('peace', () => {
|
||||||
|
const d = deal('austria', 'italy', { kind: 'peace' })
|
||||||
|
const neighbours = boardFrom([A('austria', 'tri'), A('italy', 'ven')])
|
||||||
|
|
||||||
|
it('holds while neither touches the other', () => {
|
||||||
|
const v = judge(d, orders({ austria: [mv('tri', 'alb')], italy: [mv('ven', 'pie')] }), neighbours)
|
||||||
|
expect(v.every((x) => x.kept)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is broken by moving on a unit of theirs', () => {
|
||||||
|
const v = judge(d, orders({ austria: [hold('tri')], italy: [mv('ven', 'tri')] }), neighbours)
|
||||||
|
expect(v.find((x) => x.power === 'italy')!.kept).toBe(false)
|
||||||
|
expect(v.find((x) => x.power === 'italy')!.why).toContain('tri')
|
||||||
|
expect(v.find((x) => x.power === 'austria')!.kept).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is broken by helping yourself to an empty centre of theirs', () => {
|
||||||
|
/*
|
||||||
|
* The centre is undefended and nobody is home. That is not a loophole in
|
||||||
|
* peace, it is the most obvious way to break it, so ownership counts as
|
||||||
|
* well as occupation.
|
||||||
|
*/
|
||||||
|
const own = new Map([['tri', 'austria']]) as Map<string, Power>
|
||||||
|
const empty = boardFrom([A('italy', 'ven')])
|
||||||
|
const v = judge(d, orders({ italy: [mv('ven', 'tri')] }), empty, own)
|
||||||
|
expect(v.find((x) => x.power === 'italy')!.kept).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('a double deal', () => {
|
||||||
|
it('is two promises that cannot both be kept', () => {
|
||||||
|
const keepOut = deal('austria', 'russia', { kind: 'dmz', province: 'gal' })
|
||||||
|
const helpIn = deal('austria', 'turkey', {
|
||||||
|
kind: 'support',
|
||||||
|
mover: 'turkey',
|
||||||
|
helper: 'austria',
|
||||||
|
from: 'rum',
|
||||||
|
to: 'gal',
|
||||||
|
})
|
||||||
|
expect(conflicts(keepOut, helpIn)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is not two promises that merely look alike', () => {
|
||||||
|
const a = deal('austria', 'russia', { kind: 'dmz', province: 'gal' })
|
||||||
|
const b = deal('austria', 'turkey', { kind: 'dmz', province: 'ser' })
|
||||||
|
expect(conflicts(a, b)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is one unit promised to two different moves', () => {
|
||||||
|
const a = deal('austria', 'russia', {
|
||||||
|
kind: 'support',
|
||||||
|
mover: 'russia',
|
||||||
|
helper: 'austria',
|
||||||
|
from: 'war',
|
||||||
|
to: 'gal',
|
||||||
|
})
|
||||||
|
const b = deal('austria', 'turkey', {
|
||||||
|
kind: 'support',
|
||||||
|
mover: 'turkey',
|
||||||
|
helper: 'austria',
|
||||||
|
from: 'rum',
|
||||||
|
to: 'ser',
|
||||||
|
})
|
||||||
|
expect(conflicts(a, b)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lets a power promise different turns freely', () => {
|
||||||
|
const a = deal('austria', 'russia', { kind: 'dmz', province: 'gal' }, 1)
|
||||||
|
const b = deal('austria', 'turkey', {
|
||||||
|
kind: 'support',
|
||||||
|
mover: 'turkey',
|
||||||
|
helper: 'austria',
|
||||||
|
from: 'rum',
|
||||||
|
to: 'gal',
|
||||||
|
}, 2)
|
||||||
|
expect(conflicts(a, b)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('memory', () => {
|
||||||
|
const d = deal('austria', 'russia', { kind: 'dmz', province: 'gal' }, 3)
|
||||||
|
|
||||||
|
it('records a broken promise against the one who broke it', () => {
|
||||||
|
const v = judge(d, orders({ austria: [hold('vie')], russia: [mv('war', 'gal')] }), board)
|
||||||
|
const ledger = remember(emptyLedger(), v)
|
||||||
|
expect(look(ledger, 'austria', 'russia').broken).toBe(1)
|
||||||
|
expect(look(ledger, 'austria', 'russia').betrayals).toEqual([3])
|
||||||
|
// Austria kept it, and Russia noticed that too.
|
||||||
|
expect(look(ledger, 'russia', 'austria').kept).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not tell anybody who was not there', () => {
|
||||||
|
const v = judge(d, orders({ austria: [hold('vie')], russia: [mv('war', 'gal')] }), board)
|
||||||
|
const ledger = remember(emptyLedger(), v)
|
||||||
|
expect(look(ledger, 'italy', 'russia')).toEqual({ kept: 0, broken: 0, betrayals: [] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('trust', () => {
|
||||||
|
const stab = (turn: number) =>
|
||||||
|
judge(
|
||||||
|
deal('austria', 'russia', { kind: 'dmz', province: 'gal' }, turn),
|
||||||
|
orders({ austria: [hold('vie')], russia: [mv('war', 'gal')] }),
|
||||||
|
board,
|
||||||
|
)
|
||||||
|
const honour = (turn: number) =>
|
||||||
|
judge(
|
||||||
|
deal('austria', 'russia', { kind: 'dmz', province: 'gal' }, turn),
|
||||||
|
orders({ austria: [hold('vie')], russia: [hold('war')] }),
|
||||||
|
board,
|
||||||
|
)
|
||||||
|
|
||||||
|
it('gives a stranger the benefit of the doubt', () => {
|
||||||
|
expect(trust(emptyLedger(), 'austria', 'russia', 1)).toBeGreaterThan(0.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('thinks better of somebody who keeps their word', () => {
|
||||||
|
let l = emptyLedger()
|
||||||
|
for (const t of [1, 2, 3]) l = remember(l, honour(t))
|
||||||
|
expect(trust(l, 'austria', 'russia', 4)).toBeGreaterThan(0.9)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drops hard on a betrayal', () => {
|
||||||
|
const l = remember(emptyLedger(), stab(1))
|
||||||
|
expect(trust(l, 'austria', 'russia', 1)).toBeLessThan(0.3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lets an old betrayal fade without forgetting it', () => {
|
||||||
|
const l = remember(emptyLedger(), stab(1))
|
||||||
|
const fresh = trust(l, 'austria', 'russia', 2)
|
||||||
|
const stale = trust(l, 'austria', 'russia', 30)
|
||||||
|
expect(stale).toBeGreaterThan(fresh)
|
||||||
|
// Never fully: the count stays in the ledger for anybody who asks.
|
||||||
|
expect(look(l, 'austria', 'russia').broken).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('weighs a stab against the promises kept before it', () => {
|
||||||
|
let loyal = emptyLedger()
|
||||||
|
for (const t of [1, 2, 3, 4, 5, 6]) loyal = remember(loyal, honour(t))
|
||||||
|
const afterLong = remember(loyal, stab(7))
|
||||||
|
const afterNothing = remember(emptyLedger(), stab(7))
|
||||||
|
expect(trust(afterLong, 'austria', 'russia', 7)).toBeGreaterThan(
|
||||||
|
trust(afterNothing, 'austria', 'russia', 7),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import { base, type Power } from './map'
|
||||||
|
import type { Board, Order } from './orders'
|
||||||
|
import type { Ownership } from './turn'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The press: what powers say to each other, and what it costs to lie.
|
||||||
|
*
|
||||||
|
* Every offer here is concrete and about a named turn. That is deliberate and
|
||||||
|
* it is the whole design. Free text would need a model to read it, could not
|
||||||
|
* be checked, and could not be tested; and a game where the machine decides
|
||||||
|
* by feel whether you kept your word is a game where being betrayed feels
|
||||||
|
* arbitrary rather than infuriating. These deals can be checked against the
|
||||||
|
* orders that were actually submitted, by arithmetic, every time.
|
||||||
|
*
|
||||||
|
* Three things a power can offer, because these are the three things that
|
||||||
|
* actually change a turn:
|
||||||
|
*
|
||||||
|
* - **support**, which wins a specific province on a specific turn;
|
||||||
|
* - **a demilitarised province**, which is how two powers agree to look
|
||||||
|
* somewhere else;
|
||||||
|
* - **peace**, which is the promise not to touch each other at all.
|
||||||
|
*
|
||||||
|
* And the important part: nothing here enforces anything. A deal is a
|
||||||
|
* sentence, orders are orders, and the gap between them is the game.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Deal =
|
||||||
|
/** `helper` will support `mover`'s move from -> to. */
|
||||||
|
| { kind: 'support'; mover: Power; helper: Power; from: string; to: string }
|
||||||
|
/** Neither party enters this province. */
|
||||||
|
| { kind: 'dmz'; province: string }
|
||||||
|
/** Neither party moves against the other at all. */
|
||||||
|
| { kind: 'peace' }
|
||||||
|
|
||||||
|
export interface Proposal {
|
||||||
|
id: string
|
||||||
|
from: Power
|
||||||
|
to: Power
|
||||||
|
/** The turn it binds. A deal is always about a particular turn. */
|
||||||
|
turn: number
|
||||||
|
deal: Deal
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Reply = 'accept' | 'refuse'
|
||||||
|
|
||||||
|
/** An accepted proposal. Binding on both parties, and on neither. */
|
||||||
|
export type Agreement = Proposal
|
||||||
|
|
||||||
|
export const parties = (a: Agreement): [Power, Power] => [a.from, a.to]
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ double deals
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two agreements that cannot both be kept.
|
||||||
|
*
|
||||||
|
* This is the mechanic rather than a safeguard against one. Promising
|
||||||
|
* Galicia to Austria and supporting Russia into it on the same turn is not a
|
||||||
|
* mistake the interface should prevent -- it is a double deal, and somebody
|
||||||
|
* is going to find out. What this does is make it a *deliberate* act: a
|
||||||
|
* power can be shown, at the moment of agreeing, that it has now promised
|
||||||
|
* two things it cannot both do.
|
||||||
|
*/
|
||||||
|
export function conflicts(a: Agreement, b: Agreement): boolean {
|
||||||
|
if (a.turn !== b.turn) return false
|
||||||
|
|
||||||
|
const enters = (x: Agreement): string | null =>
|
||||||
|
x.deal.kind === 'support' ? base(x.deal.to) : null
|
||||||
|
const closed = (x: Agreement): string | null =>
|
||||||
|
x.deal.kind === 'dmz' ? base(x.deal.province) : null
|
||||||
|
|
||||||
|
// Supporting somebody into a province you have agreed to keep empty.
|
||||||
|
const one = enters(a)
|
||||||
|
const two = closed(b)
|
||||||
|
if (one !== null && two !== null && one === two) return true
|
||||||
|
|
||||||
|
const three = enters(b)
|
||||||
|
const four = closed(a)
|
||||||
|
if (three !== null && four !== null && three === four) return true
|
||||||
|
|
||||||
|
// Two different supports promised by the same power on the same turn are
|
||||||
|
// fine -- a power may have several units -- but the same unit cannot
|
||||||
|
// support two moves at once.
|
||||||
|
if (a.deal.kind === 'support' && b.deal.kind === 'support') {
|
||||||
|
if (a.deal.helper !== b.deal.helper) return false
|
||||||
|
if (a.deal.from === b.deal.from && a.deal.to === b.deal.to) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------- keeping it
|
||||||
|
|
||||||
|
export interface Verdict {
|
||||||
|
agreement: Agreement
|
||||||
|
power: Power
|
||||||
|
kept: boolean
|
||||||
|
/** In the machine's own words, for the log and for the other power. */
|
||||||
|
why: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Did each party do what it said it would?
|
||||||
|
*
|
||||||
|
* Judged on the **orders given**, never on how they turned out. A support
|
||||||
|
* that was ordered and then cut was still given; a move that was ordered and
|
||||||
|
* bounced was still attempted. Blaming a power for a bounce would make every
|
||||||
|
* alliance a lottery, and worse, would make an honest ally look like a liar
|
||||||
|
* for something the dice did.
|
||||||
|
*/
|
||||||
|
export function judge(
|
||||||
|
agreement: Agreement,
|
||||||
|
orders: ReadonlyMap<Power, readonly Order[]>,
|
||||||
|
board: Board,
|
||||||
|
/** Who owns which centre, so peace can cover the empty ones too. */
|
||||||
|
own?: Ownership,
|
||||||
|
): Verdict[] {
|
||||||
|
const deal = agreement.deal
|
||||||
|
const given = (p: Power) => orders.get(p) ?? []
|
||||||
|
|
||||||
|
if (deal.kind === 'support') {
|
||||||
|
const promised = given(deal.helper).some(
|
||||||
|
(o) =>
|
||||||
|
o.type === 'support' && base(o.from) === base(deal.from) && base(o.to) === base(deal.to),
|
||||||
|
)
|
||||||
|
const attempted = given(deal.mover).some(
|
||||||
|
(o) => o.type === 'move' && base(o.at) === base(deal.from) && base(o.to) === base(deal.to),
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
agreement,
|
||||||
|
power: deal.helper,
|
||||||
|
kept: promised,
|
||||||
|
why: promised ? 'gave the support' : 'did not order the support',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agreement,
|
||||||
|
power: deal.mover,
|
||||||
|
kept: attempted,
|
||||||
|
why: attempted ? 'made the move' : 'did not make the move it asked help for',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deal.kind === 'dmz') {
|
||||||
|
const province = base(deal.province)
|
||||||
|
return parties(agreement).map((power) => {
|
||||||
|
const went = given(power).some((o) => o.type === 'move' && base(o.to) === province)
|
||||||
|
return {
|
||||||
|
agreement,
|
||||||
|
power,
|
||||||
|
kept: !went,
|
||||||
|
why: went ? `moved into ${province}` : `stayed out of ${province}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Peace. Touching anything of theirs breaks it: the provinces their units
|
||||||
|
* are standing in, and the centres they own even when nobody is standing
|
||||||
|
* there. Walking into an undefended centre and calling it peace because
|
||||||
|
* nobody was home is exactly the move this has to catch.
|
||||||
|
*/
|
||||||
|
return parties(agreement).map((power) => {
|
||||||
|
const them = power === agreement.from ? agreement.to : agreement.from
|
||||||
|
const theirs = new Set(
|
||||||
|
[...board.entries()].filter(([, u]) => u.power === them).map(([p]) => p),
|
||||||
|
)
|
||||||
|
if (own) for (const [p, who] of own) if (who === them) theirs.add(p)
|
||||||
|
const against = given(power).find((o) => o.type === 'move' && theirs.has(base(o.to)))
|
||||||
|
return {
|
||||||
|
agreement,
|
||||||
|
power,
|
||||||
|
kept: against === undefined,
|
||||||
|
why:
|
||||||
|
against && against.type === 'move'
|
||||||
|
? `moved on ${base(against.to)}`
|
||||||
|
: 'kept off them entirely',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ memory
|
||||||
|
|
||||||
|
export interface Record_ {
|
||||||
|
kept: number
|
||||||
|
broken: number
|
||||||
|
/** The turns it went wrong, so a power can say when rather than how often. */
|
||||||
|
betrayals: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What each power thinks of each other power. Directional, and it matters. */
|
||||||
|
export type Ledger = Map<string, Record_>
|
||||||
|
|
||||||
|
const key = (observer: Power, subject: Power) => `${observer}:${subject}`
|
||||||
|
|
||||||
|
export const emptyLedger = (): Ledger => new Map()
|
||||||
|
|
||||||
|
export function look(ledger: Ledger, observer: Power, subject: Power): Record_ {
|
||||||
|
return ledger.get(key(observer, subject)) ?? { kept: 0, broken: 0, betrayals: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the turn into everybody's memory.
|
||||||
|
*
|
||||||
|
* Only the other party to a deal learns from it directly, which is the point
|
||||||
|
* of a game played in private. What the rest of the board knows is what it
|
||||||
|
* can see -- and that is handled elsewhere, by looking at the map, not here.
|
||||||
|
*/
|
||||||
|
export function remember(ledger: Ledger, verdicts: readonly Verdict[]): Ledger {
|
||||||
|
const next: Ledger = new Map(ledger)
|
||||||
|
for (const v of verdicts) {
|
||||||
|
const other = v.power === v.agreement.from ? v.agreement.to : v.agreement.from
|
||||||
|
const at = key(other, v.power)
|
||||||
|
const was = next.get(at) ?? { kept: 0, broken: 0, betrayals: [] }
|
||||||
|
next.set(at, {
|
||||||
|
kept: was.kept + (v.kept ? 1 : 0),
|
||||||
|
broken: was.broken + (v.kept ? 0 : 1),
|
||||||
|
betrayals: v.kept ? was.betrayals : [...was.betrayals, v.agreement.turn],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far one power will trust another, from nothing to complete.
|
||||||
|
*
|
||||||
|
* A power nobody has dealt with sits at 0.6 rather than 0.5: somebody you
|
||||||
|
* have never dealt with is a better bet than somebody who has already lied
|
||||||
|
* to you once, and a game where every stranger is treated as a proven liar
|
||||||
|
* never gets started at all.
|
||||||
|
*
|
||||||
|
* Betrayals also fade. Not to nothing -- the count never leaves the ledger --
|
||||||
|
* but a stab in 1901 should not still be the loudest fact about a power in
|
||||||
|
* 1910, or no alliance could ever re-form, and re-forming a broken alliance
|
||||||
|
* out of pure need is one of the best things this game does.
|
||||||
|
*/
|
||||||
|
export function trust(ledger: Ledger, observer: Power, subject: Power, turn: number): number {
|
||||||
|
const r = look(ledger, observer, subject)
|
||||||
|
if (r.kept === 0 && r.broken === 0) return 0.6
|
||||||
|
|
||||||
|
const weigh = (t: number) => 1 / (1 + Math.max(0, turn - t) / 8)
|
||||||
|
const hurt = r.betrayals.reduce((n, t) => n + weigh(t), 0)
|
||||||
|
const score = (r.kept - 2 * hurt) / (r.kept + r.broken)
|
||||||
|
return Math.max(0, Math.min(1, (score + 1) / 2))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user