Repository navigation
Rozbudowa spotkań i głosowań w podsumowaniu gry #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
903298f
0073d59
df8bbb9
0d79c2a
5164b0e
dc5d4f8
b49f245
cc97a8b
b632208
9b4980d
e315d0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { getDeathsDuringMeetings } from './_meetingDeaths'; | ||
|
|
||
| const meetings = [ | ||
| { meetingNumber: 1, deathsSinceLastMeeting: [] }, | ||
| { meetingNumber: 2, deathsSinceLastMeeting: ['A (zastrzelony)'] }, | ||
| ]; | ||
|
|
||
| test('places a guess victim in the meeting preceding the death summary', () => { | ||
| expect(getDeathsDuringMeetings(meetings, [{ type: 'kill', target: { name: 'A' }, detail: '{"isGuess":true}' }]).get(1)).toEqual(['A']); | ||
| }); | ||
|
|
||
| test('includes final meeting executions even without a following summary', () => { | ||
| expect(getDeathsDuringMeetings(meetings, [{ type: 'kill', target: { name: 'B' }, detail: '{"causeOfDeath":"Jailor"}' }]).get(2)).toEqual(['B']); | ||
| }); | ||
|
|
||
| test('ignores ordinary kills and malformed details', () => { | ||
| expect(getDeathsDuringMeetings(meetings, [ | ||
| { type: 'kill', target: { name: 'A' }, detail: null }, | ||
| { type: 'kill', target: { name: 'A' }, detail: 'invalid' }, | ||
| ]).size).toBe(0); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| type Meeting = { meetingNumber: number; deathsSinceLastMeeting: string[] }; | ||
| type Action = { type: string; detail: string | null; target: { name: string } | null }; | ||
|
|
||
| export function getDeathsDuringMeetings(meetings: Meeting[], actions: Action[]): Map<number, string[]> { | ||
| const result = new Map<number, string[]>(); | ||
| for (const action of actions) { | ||
| if (action.type !== 'kill' || !action.target || !action.detail) continue; | ||
| let detail: unknown; | ||
| try { detail = JSON.parse(action.detail); } catch { continue; } | ||
| if (!detail || typeof detail !== 'object') continue; | ||
| const isMeetingDeath = ('isGuess' in detail && detail.isGuess === true) || | ||
| ('causeOfDeath' in detail && ['Jailor', 'Prosecutor', 'Guess', 'Misguess'].includes(String(detail.causeOfDeath))); | ||
| if (!isMeetingDeath) continue; | ||
| const name = action.target.name; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Ustal faktyczną ofiarę nietrafionego zgadywania zamiast zawsze używać Kontrakt w Przykład: na spotkaniu #1 A błędnie zgaduje rolę B, a podsumowanie spotkania #2 zawiera zgon A. Helper szuka B w podsumowaniach; jeśli B przeżyje grę, wybiera ostatnie spotkanie i zwraca |
||
| // Meeting deaths appear in the following meeting's death summary. The final | ||
| // meeting has no following summary, so its guess/execution victims need actions. | ||
| const nextIndex = meetings.findIndex(meeting => meeting.deathsSinceLastMeeting.some(death => death.split(' (')[0].trim() === name)); | ||
| const meeting = nextIndex > 0 ? meetings[nextIndex - 1] : nextIndex === -1 ? meetings[meetings.length - 1] : undefined; | ||
| if (!meeting) continue; | ||
| const deaths = result.get(meeting.meetingNumber) ?? []; | ||
| if (!deaths.includes(name)) deaths.push(name); | ||
| result.set(meeting.meetingNumber, deaths); | ||
| } | ||
| return result; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { getMeetingParticipants } from './meetingParticipants'; | ||
| import type { UIMeetingData } from '../_services/games/types'; | ||
|
|
||
| const meeting = (overrides: Partial<UIMeetingData>): UIMeetingData => ({ | ||
| meetingNumber: 1, deathsSinceLastMeeting: [], votes: {}, skipVotes: [], noVotes: [], wasTie: false, wasBlessed: false, ...overrides, | ||
| }); | ||
|
|
||
| test('includes living players when the final meeting ends before voting', () => { | ||
| const meetings = [meeting({ votes: { B: ['A'] }, exiledPlayer: 'B' }), meeting({ meetingNumber: 2, deathsSinceLastMeeting: ['C (zabity)'] })]; | ||
| expect(getMeetingParticipants(meetings, ['A', 'B', 'C', 'D']).get(2)).toEqual(['A', 'D']); | ||
| }); | ||
|
|
||
| test('does not add the full roster to a completed voting meeting', () => { | ||
| expect(getMeetingParticipants([meeting({ votes: { B: ['A'] } })], ['A', 'B', 'C']).get(1)).toEqual(['B', 'A']); | ||
| }); | ||
|
|
||
| test('restores players recorded as participants after an earlier death', () => { | ||
| const meetings = [meeting({ deathsSinceLastMeeting: ['B'] }), meeting({ meetingNumber: 2, noVotes: ['B'] })]; | ||
| expect(getMeetingParticipants(meetings, ['A', 'B']).get(2)).toEqual(['A', 'B']); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import type { UIMeetingData } from '../_services/games/types'; | ||
|
|
||
| /** Recover the final unvoted meeting from the roster and recorded deaths/exiles. */ | ||
| export function getMeetingParticipants(meetings: UIMeetingData[], roster: string[]): Map<number, string[]> { | ||
| const alive = new Set(roster); | ||
| const result = new Map<number, string[]>(); | ||
| meetings.forEach((meeting, index) => { | ||
| meeting.deathsSinceLastMeeting.forEach(death => alive.delete(death.split(' (')[0].trim())); | ||
| const recorded = new Set([ | ||
| ...Object.keys(meeting.votes), | ||
| ...Object.values(meeting.votes).flat(), | ||
| ...meeting.skipVotes, | ||
| ...meeting.noVotes, | ||
| ...(meeting.deathsDuringMeeting ?? []), | ||
| ]); | ||
| // A recorded participant may have been revived since an earlier meeting. | ||
| recorded.forEach(name => alive.add(name)); | ||
| const hasBallots = Object.values(meeting.votes).some(voters => voters.length > 0) || meeting.skipVotes.length > 0; | ||
| result.set(meeting.meetingNumber, index === meetings.length - 1 && !hasBallots ? Array.from(alive) : Array.from(recorded)); | ||
| if (meeting.exiledPlayer) alive.delete(meeting.exiledPlayer); | ||
| meeting.deathsDuringMeeting?.forEach(name => alive.delete(name)); | ||
| }); | ||
| return result; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.