@snapshot-labs/snapshot.js 0.12.49 → 0.12.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/snapshot.cjs.js +119 -0
- package/dist/snapshot.esm.js +119 -0
- package/dist/snapshot.min.js +6 -6
- package/dist/src/index.d.ts +1 -0
- package/dist/src/utils.d.ts +1 -0
- package/dist/src/voting/copeland.d.ts +18 -0
- package/dist/src/voting/index.d.ts +2 -0
- package/dist/src/voting/quadratic.d.ts +4 -4
- package/dist/src/voting/types.d.ts +10 -26
- package/package.json +1 -1
- package/src/schemas/proposal.json +1 -0
- package/src/sign/index.ts +1 -0
- package/src/voting/__snapshots__/copeland.spec.js.snap +134 -0
- package/src/voting/copeland.spec.js +129 -0
- package/src/voting/copeland.ts +155 -0
- package/src/voting/examples/copeland.json +32 -0
- package/src/voting/index.ts +2 -0
- package/src/voting/quadratic.ts +4 -4
- package/src/voting/types.ts +8 -29
package/dist/src/index.d.ts
CHANGED
|
@@ -831,6 +831,7 @@ declare const _default: {
|
|
|
831
831
|
approval: typeof import("./voting/approval").default;
|
|
832
832
|
quadratic: typeof import("./voting/quadratic").default;
|
|
833
833
|
'ranked-choice': typeof import("./voting/rankedChoice").default;
|
|
834
|
+
copeland: typeof import("./voting/copeland").default;
|
|
834
835
|
weighted: typeof import("./voting/weighted").default;
|
|
835
836
|
basic: typeof import("./voting/singleChoice").default;
|
|
836
837
|
};
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ declare const _default: {
|
|
|
63
63
|
approval: typeof import("./voting/approval").default;
|
|
64
64
|
quadratic: typeof import("./voting/quadratic").default;
|
|
65
65
|
'ranked-choice': typeof import("./voting/rankedChoice").default;
|
|
66
|
+
copeland: typeof import("./voting/copeland").default;
|
|
66
67
|
weighted: typeof import("./voting/weighted").default;
|
|
67
68
|
basic: typeof import("./voting/singleChoice").default;
|
|
68
69
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Strategy, RankedChoiceVote } from './types';
|
|
2
|
+
export default class CopelandVoting {
|
|
3
|
+
proposal: {
|
|
4
|
+
choices: string[];
|
|
5
|
+
};
|
|
6
|
+
votes: RankedChoiceVote[];
|
|
7
|
+
strategies: Strategy[];
|
|
8
|
+
selected: number[];
|
|
9
|
+
constructor(proposal: {
|
|
10
|
+
choices: string[];
|
|
11
|
+
}, votes: RankedChoiceVote[], strategies: Strategy[], selected: number[]);
|
|
12
|
+
static isValidChoice(voteChoice: number[], proposalChoices: string[]): boolean;
|
|
13
|
+
getValidVotes(): RankedChoiceVote[];
|
|
14
|
+
getScores(): number[];
|
|
15
|
+
getScoresByStrategy(): number[][];
|
|
16
|
+
getScoresTotal(): number;
|
|
17
|
+
getChoiceString(): string;
|
|
18
|
+
}
|
|
@@ -2,12 +2,14 @@ import singleChoice from './singleChoice';
|
|
|
2
2
|
import approval from './approval';
|
|
3
3
|
import quadratic from './quadratic';
|
|
4
4
|
import rankedChoice from './rankedChoice';
|
|
5
|
+
import copeland from './copeland';
|
|
5
6
|
import weighted from './weighted';
|
|
6
7
|
declare const _default: {
|
|
7
8
|
'single-choice': typeof singleChoice;
|
|
8
9
|
approval: typeof approval;
|
|
9
10
|
quadratic: typeof quadratic;
|
|
10
11
|
'ranked-choice': typeof rankedChoice;
|
|
12
|
+
copeland: typeof copeland;
|
|
11
13
|
weighted: typeof weighted;
|
|
12
14
|
basic: typeof singleChoice;
|
|
13
15
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QuadraticVote,
|
|
1
|
+
import { QuadraticVote, ChoiceMap, Strategy } from './types';
|
|
2
2
|
export declare function calcPercentageOfSum(part: number, wholeArray: number[]): number;
|
|
3
3
|
export declare function calcSqrt(percentageWeight: number, votingPower: number): number;
|
|
4
4
|
export default class QuadraticVoting {
|
|
@@ -7,11 +7,11 @@ export default class QuadraticVoting {
|
|
|
7
7
|
};
|
|
8
8
|
votes: QuadraticVote[];
|
|
9
9
|
strategies: Strategy[];
|
|
10
|
-
selected:
|
|
10
|
+
selected: ChoiceMap;
|
|
11
11
|
constructor(proposal: {
|
|
12
12
|
choices: string[];
|
|
13
|
-
}, votes: QuadraticVote[], strategies: Strategy[], selected:
|
|
14
|
-
static isValidChoice(voteChoice:
|
|
13
|
+
}, votes: QuadraticVote[], strategies: Strategy[], selected: ChoiceMap);
|
|
14
|
+
static isValidChoice(voteChoice: ChoiceMap, proposalChoices: string[]): boolean;
|
|
15
15
|
getValidVotes(): QuadraticVote[];
|
|
16
16
|
getScores(): number[];
|
|
17
17
|
getScoresByStrategy(): number[][];
|
|
@@ -3,33 +3,17 @@ export interface Strategy {
|
|
|
3
3
|
network: string;
|
|
4
4
|
params: Record<string, unknown>;
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
choice:
|
|
6
|
+
interface BaseVote<TChoice> {
|
|
7
|
+
choice: TChoice;
|
|
8
8
|
balance: number;
|
|
9
9
|
scores: number[];
|
|
10
10
|
}
|
|
11
|
-
export
|
|
12
|
-
choice: number[];
|
|
13
|
-
balance: number;
|
|
14
|
-
scores: number[];
|
|
15
|
-
}
|
|
16
|
-
export interface RankedChoiceVote {
|
|
17
|
-
choice: number[];
|
|
18
|
-
balance: number;
|
|
19
|
-
scores: number[];
|
|
20
|
-
}
|
|
21
|
-
export interface QuadraticChoice {
|
|
11
|
+
export type ChoiceMap = {
|
|
22
12
|
[key: string]: number;
|
|
23
|
-
}
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export
|
|
30
|
-
choice: {
|
|
31
|
-
[key: string]: number;
|
|
32
|
-
};
|
|
33
|
-
balance: number;
|
|
34
|
-
scores: number[];
|
|
35
|
-
}
|
|
13
|
+
};
|
|
14
|
+
export type SingleChoiceVote = BaseVote<number>;
|
|
15
|
+
export type ApprovalVote = BaseVote<number[]>;
|
|
16
|
+
export type RankedChoiceVote = BaseVote<number[]>;
|
|
17
|
+
export type QuadraticVote = BaseVote<ChoiceMap>;
|
|
18
|
+
export type WeightedVote = BaseVote<ChoiceMap>;
|
|
19
|
+
export {};
|
package/package.json
CHANGED
package/src/sign/index.ts
CHANGED
|
@@ -124,6 +124,7 @@ export default class Client {
|
|
|
124
124
|
address: string,
|
|
125
125
|
message: Proposal
|
|
126
126
|
) {
|
|
127
|
+
if (!message.labels) message.labels = [];
|
|
127
128
|
if (!message.discussion) message.discussion = '';
|
|
128
129
|
if (!message.app) message.app = '';
|
|
129
130
|
if (!message.privacy) message.privacy = '';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`Partial ranking 1`] = `
|
|
4
|
+
[
|
|
5
|
+
4,
|
|
6
|
+
6,
|
|
7
|
+
2,
|
|
8
|
+
0,
|
|
9
|
+
]
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
exports[`getScores 1`] = `
|
|
13
|
+
[
|
|
14
|
+
5,
|
|
15
|
+
5,
|
|
16
|
+
2,
|
|
17
|
+
0,
|
|
18
|
+
]
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
exports[`getScores 2`] = `
|
|
22
|
+
[
|
|
23
|
+
5,
|
|
24
|
+
5,
|
|
25
|
+
2,
|
|
26
|
+
0,
|
|
27
|
+
]
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
exports[`getScores 3`] = `
|
|
31
|
+
[
|
|
32
|
+
5,
|
|
33
|
+
5,
|
|
34
|
+
2,
|
|
35
|
+
0,
|
|
36
|
+
]
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
exports[`getScores 4`] = `
|
|
40
|
+
[
|
|
41
|
+
5,
|
|
42
|
+
5,
|
|
43
|
+
2,
|
|
44
|
+
0,
|
|
45
|
+
]
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
exports[`getScoresByStrategy 1`] = `
|
|
49
|
+
[
|
|
50
|
+
[
|
|
51
|
+
5,
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
5,
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
2,
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
0,
|
|
61
|
+
],
|
|
62
|
+
]
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
exports[`getScoresByStrategy 2`] = `
|
|
66
|
+
[
|
|
67
|
+
[
|
|
68
|
+
5,
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
5,
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
2,
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
0,
|
|
78
|
+
],
|
|
79
|
+
]
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
exports[`getScoresByStrategy 3`] = `
|
|
83
|
+
[
|
|
84
|
+
[
|
|
85
|
+
5,
|
|
86
|
+
5,
|
|
87
|
+
5,
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
5,
|
|
91
|
+
5,
|
|
92
|
+
5,
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
2,
|
|
96
|
+
2,
|
|
97
|
+
2,
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
0,
|
|
101
|
+
0,
|
|
102
|
+
0,
|
|
103
|
+
],
|
|
104
|
+
]
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
exports[`getScoresByStrategy 4`] = `
|
|
108
|
+
[
|
|
109
|
+
[
|
|
110
|
+
5,
|
|
111
|
+
5,
|
|
112
|
+
5,
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
5,
|
|
116
|
+
5,
|
|
117
|
+
5,
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
2,
|
|
121
|
+
2,
|
|
122
|
+
2,
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
0,
|
|
126
|
+
0,
|
|
127
|
+
0,
|
|
128
|
+
],
|
|
129
|
+
]
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
exports[`getScoresTotal 1`] = `4`;
|
|
133
|
+
|
|
134
|
+
exports[`getScoresTotal 2`] = `12`;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import CopelandVoting from './copeland';
|
|
3
|
+
import example from './examples/copeland.json';
|
|
4
|
+
|
|
5
|
+
// Helper function to create a more complex example with multiple strategies
|
|
6
|
+
const example2 = () => {
|
|
7
|
+
const proposal = {
|
|
8
|
+
choices: ['Alice', 'Bob', 'Carol', 'David']
|
|
9
|
+
};
|
|
10
|
+
const strategies = [
|
|
11
|
+
{ name: 'ticket', network: '1', params: {} },
|
|
12
|
+
{ name: 'ticket', network: '1', params: {} },
|
|
13
|
+
{ name: 'ticket', network: '1', params: {} }
|
|
14
|
+
];
|
|
15
|
+
const votes = example.votes.map((vote) => ({
|
|
16
|
+
choice: vote.choice,
|
|
17
|
+
balance: 3,
|
|
18
|
+
scores: [1, 1, 1]
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
proposal,
|
|
23
|
+
strategies,
|
|
24
|
+
votes
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Generate a set of votes including some invalid choices
|
|
29
|
+
const votesWithInvalidChoices = () => {
|
|
30
|
+
const invalidVotes = [
|
|
31
|
+
{ choice: [0, 1], balance: 1, scores: [1] },
|
|
32
|
+
{ choice: [1, 5], balance: 1, scores: [1] },
|
|
33
|
+
{ choice: [1, 1], balance: 1, scores: [1] },
|
|
34
|
+
{ choice: [], balance: 1, scores: [1] },
|
|
35
|
+
{ choice: [1, 2, 3, 4, 5], balance: 1, scores: [1] }
|
|
36
|
+
];
|
|
37
|
+
return [...invalidVotes, ...example.votes];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Generate a set of votes including some invalid choices for the multi-strategy example
|
|
41
|
+
const votesWithInvalidChoices2 = () => {
|
|
42
|
+
const invalidVotes = [
|
|
43
|
+
{ choice: [0, 1], balance: 3, scores: [1, 1, 1] },
|
|
44
|
+
{ choice: [1, 5], balance: 3, scores: [1, 1, 1] },
|
|
45
|
+
{ choice: [1, 1], balance: 3, scores: [1, 1, 1] },
|
|
46
|
+
{ choice: [], balance: 3, scores: [1, 1, 1] },
|
|
47
|
+
{ choice: [1, 2, 3, 4, 5], balance: 3, scores: [1, 1, 1] }
|
|
48
|
+
];
|
|
49
|
+
return [...invalidVotes, ...example2().votes];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Test cases for getScores method
|
|
53
|
+
test.each([
|
|
54
|
+
[example.proposal, example.votes, example.strategies],
|
|
55
|
+
[example.proposal, votesWithInvalidChoices(), example.strategies],
|
|
56
|
+
[example2().proposal, example2().votes, example2().strategies],
|
|
57
|
+
[example2().proposal, votesWithInvalidChoices2(), example2().strategies]
|
|
58
|
+
])('getScores', (proposal, votes, strategies) => {
|
|
59
|
+
const copeland = new CopelandVoting(
|
|
60
|
+
proposal,
|
|
61
|
+
votes,
|
|
62
|
+
strategies,
|
|
63
|
+
example.selectedChoice
|
|
64
|
+
);
|
|
65
|
+
expect(copeland.getScores()).toMatchSnapshot();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Test cases for getScoresByStrategy method
|
|
69
|
+
test.each([
|
|
70
|
+
[example.proposal, example.votes, example.strategies],
|
|
71
|
+
[example.proposal, votesWithInvalidChoices(), example.strategies],
|
|
72
|
+
[example2().proposal, example2().votes, example2().strategies],
|
|
73
|
+
[example2().proposal, votesWithInvalidChoices2(), example2().strategies]
|
|
74
|
+
])('getScoresByStrategy', (proposal, votes, strategies) => {
|
|
75
|
+
const copeland = new CopelandVoting(
|
|
76
|
+
proposal,
|
|
77
|
+
votes,
|
|
78
|
+
strategies,
|
|
79
|
+
example.selectedChoice
|
|
80
|
+
);
|
|
81
|
+
expect(copeland.getScoresByStrategy()).toMatchSnapshot();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Test cases for getScoresTotal method
|
|
85
|
+
test.each([
|
|
86
|
+
[example.proposal, example.votes, example.strategies],
|
|
87
|
+
[example2().proposal, example2().votes, example2().strategies]
|
|
88
|
+
])('getScoresTotal', (proposal, votes, strategies) => {
|
|
89
|
+
const copeland = new CopelandVoting(
|
|
90
|
+
proposal,
|
|
91
|
+
votes,
|
|
92
|
+
strategies,
|
|
93
|
+
example.selectedChoice
|
|
94
|
+
);
|
|
95
|
+
expect(copeland.getScoresTotal()).toMatchSnapshot();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Test cases for getChoiceString method
|
|
99
|
+
test.each([
|
|
100
|
+
[[1, 2], 'Alice, Bob'],
|
|
101
|
+
[[4, 2, 3, 1], 'David, Bob, Carol, Alice']
|
|
102
|
+
])('getChoiceString %s', (selected, expected) => {
|
|
103
|
+
const copeland = new CopelandVoting(
|
|
104
|
+
example.proposal,
|
|
105
|
+
example.votes,
|
|
106
|
+
example.strategies,
|
|
107
|
+
selected
|
|
108
|
+
);
|
|
109
|
+
expect(copeland.getChoiceString()).toBe(expected);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Test case for partial ranking
|
|
113
|
+
test('Partial ranking', () => {
|
|
114
|
+
const partialVotes = [
|
|
115
|
+
...example.votes,
|
|
116
|
+
{
|
|
117
|
+
choice: [2, 1],
|
|
118
|
+
balance: 1,
|
|
119
|
+
scores: [1]
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
const copeland = new CopelandVoting(
|
|
123
|
+
example.proposal,
|
|
124
|
+
partialVotes,
|
|
125
|
+
example.strategies,
|
|
126
|
+
example.selectedChoice
|
|
127
|
+
);
|
|
128
|
+
expect(copeland.getScores()).toMatchSnapshot();
|
|
129
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Strategy, RankedChoiceVote } from './types';
|
|
2
|
+
|
|
3
|
+
// CopelandVoting implements ranked choice voting using Copeland's method
|
|
4
|
+
// This method compares each pair of choices and awards points based on pairwise victories
|
|
5
|
+
export default class CopelandVoting {
|
|
6
|
+
proposal: { choices: string[] };
|
|
7
|
+
votes: RankedChoiceVote[];
|
|
8
|
+
strategies: Strategy[];
|
|
9
|
+
selected: number[];
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
proposal: { choices: string[] },
|
|
13
|
+
votes: RankedChoiceVote[],
|
|
14
|
+
strategies: Strategy[],
|
|
15
|
+
selected: number[]
|
|
16
|
+
) {
|
|
17
|
+
this.proposal = proposal;
|
|
18
|
+
this.votes = votes;
|
|
19
|
+
this.strategies = strategies;
|
|
20
|
+
this.selected = selected;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validates if a vote choice is valid for the given proposal
|
|
24
|
+
// Allows partial ranking (not all choices need to be ranked)
|
|
25
|
+
static isValidChoice(
|
|
26
|
+
voteChoice: number[],
|
|
27
|
+
proposalChoices: string[]
|
|
28
|
+
): boolean {
|
|
29
|
+
if (
|
|
30
|
+
!Array.isArray(voteChoice) ||
|
|
31
|
+
voteChoice.length === 0 ||
|
|
32
|
+
voteChoice.length > proposalChoices.length ||
|
|
33
|
+
new Set(voteChoice).size !== voteChoice.length
|
|
34
|
+
) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return voteChoice.every(
|
|
39
|
+
(choice) =>
|
|
40
|
+
Number.isInteger(choice) &&
|
|
41
|
+
choice >= 1 &&
|
|
42
|
+
choice <= proposalChoices.length
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Returns only the valid votes
|
|
47
|
+
getValidVotes(): RankedChoiceVote[] {
|
|
48
|
+
return this.votes.filter((vote) =>
|
|
49
|
+
CopelandVoting.isValidChoice(vote.choice, this.proposal.choices)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Calculates the Copeland scores for each choice
|
|
54
|
+
getScores(): number[] {
|
|
55
|
+
const validVotes = this.getValidVotes();
|
|
56
|
+
const choicesCount = this.proposal.choices.length;
|
|
57
|
+
const pairwiseComparisons = Array.from({ length: choicesCount }, () =>
|
|
58
|
+
Array(choicesCount).fill(0)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Calculate pairwise comparisons
|
|
62
|
+
for (const vote of validVotes) {
|
|
63
|
+
for (let i = 0; i < vote.choice.length; i++) {
|
|
64
|
+
for (let j = i + 1; j < vote.choice.length; j++) {
|
|
65
|
+
const winner = vote.choice[i] - 1;
|
|
66
|
+
const loser = vote.choice[j] - 1;
|
|
67
|
+
pairwiseComparisons[winner][loser] += vote.balance;
|
|
68
|
+
pairwiseComparisons[loser][winner] -= vote.balance;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Calculate Copeland scores
|
|
74
|
+
const scores = Array(choicesCount).fill(0);
|
|
75
|
+
for (let i = 0; i < choicesCount; i++) {
|
|
76
|
+
for (let j = 0; j < choicesCount; j++) {
|
|
77
|
+
if (i !== j) {
|
|
78
|
+
if (pairwiseComparisons[i][j] > 0) {
|
|
79
|
+
scores[i]++;
|
|
80
|
+
} else if (pairwiseComparisons[i][j] < 0) {
|
|
81
|
+
scores[j]++;
|
|
82
|
+
} else {
|
|
83
|
+
scores[i] += 0.5;
|
|
84
|
+
scores[j] += 0.5;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return scores;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Calculates the Copeland scores for each choice, broken down by strategy
|
|
94
|
+
getScoresByStrategy(): number[][] {
|
|
95
|
+
const validVotes = this.getValidVotes();
|
|
96
|
+
const choicesCount = this.proposal.choices.length;
|
|
97
|
+
const strategiesCount = this.strategies.length;
|
|
98
|
+
const pairwiseComparisons = Array.from({ length: choicesCount }, () =>
|
|
99
|
+
Array.from({ length: choicesCount }, () => Array(strategiesCount).fill(0))
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Calculate pairwise comparisons for each strategy
|
|
103
|
+
for (const vote of validVotes) {
|
|
104
|
+
for (let i = 0; i < vote.choice.length; i++) {
|
|
105
|
+
for (let j = i + 1; j < vote.choice.length; j++) {
|
|
106
|
+
const winner = vote.choice[i] - 1;
|
|
107
|
+
const loser = vote.choice[j] - 1;
|
|
108
|
+
for (let s = 0; s < strategiesCount; s++) {
|
|
109
|
+
pairwiseComparisons[winner][loser][s] += vote.scores[s];
|
|
110
|
+
pairwiseComparisons[loser][winner][s] -= vote.scores[s];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Calculate Copeland scores for each strategy
|
|
117
|
+
const scores = Array.from({ length: choicesCount }, () =>
|
|
118
|
+
Array(strategiesCount).fill(0)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < choicesCount; i++) {
|
|
122
|
+
for (let j = 0; j < choicesCount; j++) {
|
|
123
|
+
if (i !== j) {
|
|
124
|
+
for (let s = 0; s < strategiesCount; s++) {
|
|
125
|
+
if (pairwiseComparisons[i][j][s] > 0) {
|
|
126
|
+
scores[i][s]++;
|
|
127
|
+
} else if (pairwiseComparisons[i][j][s] < 0) {
|
|
128
|
+
scores[j][s]++;
|
|
129
|
+
} else {
|
|
130
|
+
scores[i][s] += 0.5;
|
|
131
|
+
scores[j][s] += 0.5;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return scores;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Calculates the total score (sum of all valid vote balances)
|
|
142
|
+
getScoresTotal(): number {
|
|
143
|
+
return this.getValidVotes().reduce(
|
|
144
|
+
(total, vote) => total + vote.balance,
|
|
145
|
+
0
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Returns a string representation of the selected choices
|
|
150
|
+
getChoiceString(): string {
|
|
151
|
+
return this.selected
|
|
152
|
+
.map((choice) => this.proposal.choices[choice - 1])
|
|
153
|
+
.join(', ');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"proposal": {
|
|
3
|
+
"choices": ["Alice", "Bob", "Carol", "David"]
|
|
4
|
+
},
|
|
5
|
+
"strategies": [{ "name": "ticket", "network": "1", "params": {} }],
|
|
6
|
+
"scores": [2.5, 2, 0.5, 1],
|
|
7
|
+
"scoresByStrategy": [[2.5], [2], [0.5], [1]],
|
|
8
|
+
"scoresTotal": 4,
|
|
9
|
+
"selectedChoice": [1, 2],
|
|
10
|
+
"votes": [
|
|
11
|
+
{
|
|
12
|
+
"choice": [1, 2, 3, 4],
|
|
13
|
+
"balance": 1,
|
|
14
|
+
"scores": [1]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"choice": [2, 1, 3, 4],
|
|
18
|
+
"balance": 1,
|
|
19
|
+
"scores": [1]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"choice": [1, 3, 2, 4],
|
|
23
|
+
"balance": 1,
|
|
24
|
+
"scores": [1]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"choice": [2, 1],
|
|
28
|
+
"balance": 1,
|
|
29
|
+
"scores": [1]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
package/src/voting/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import singleChoice from './singleChoice';
|
|
|
2
2
|
import approval from './approval';
|
|
3
3
|
import quadratic from './quadratic';
|
|
4
4
|
import rankedChoice from './rankedChoice';
|
|
5
|
+
import copeland from './copeland';
|
|
5
6
|
import weighted from './weighted';
|
|
6
7
|
|
|
7
8
|
export default {
|
|
@@ -9,6 +10,7 @@ export default {
|
|
|
9
10
|
approval,
|
|
10
11
|
quadratic,
|
|
11
12
|
'ranked-choice': rankedChoice,
|
|
13
|
+
copeland,
|
|
12
14
|
weighted,
|
|
13
15
|
basic: singleChoice
|
|
14
16
|
};
|
package/src/voting/quadratic.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QuadraticVote,
|
|
1
|
+
import { QuadraticVote, ChoiceMap, Strategy } from './types';
|
|
2
2
|
|
|
3
3
|
export function calcPercentageOfSum(
|
|
4
4
|
part: number,
|
|
@@ -34,13 +34,13 @@ export default class QuadraticVoting {
|
|
|
34
34
|
proposal: { choices: string[] };
|
|
35
35
|
votes: QuadraticVote[];
|
|
36
36
|
strategies: Strategy[];
|
|
37
|
-
selected:
|
|
37
|
+
selected: ChoiceMap;
|
|
38
38
|
|
|
39
39
|
constructor(
|
|
40
40
|
proposal: { choices: string[] },
|
|
41
41
|
votes: QuadraticVote[],
|
|
42
42
|
strategies: Strategy[],
|
|
43
|
-
selected:
|
|
43
|
+
selected: ChoiceMap
|
|
44
44
|
) {
|
|
45
45
|
this.proposal = proposal;
|
|
46
46
|
this.votes = votes;
|
|
@@ -49,7 +49,7 @@ export default class QuadraticVoting {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
static isValidChoice(
|
|
52
|
-
voteChoice:
|
|
52
|
+
voteChoice: ChoiceMap,
|
|
53
53
|
proposalChoices: string[]
|
|
54
54
|
): boolean {
|
|
55
55
|
return (
|
package/src/voting/types.ts
CHANGED
|
@@ -4,36 +4,15 @@ export interface Strategy {
|
|
|
4
4
|
params: Record<string, unknown>;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
choice:
|
|
7
|
+
interface BaseVote<TChoice> {
|
|
8
|
+
choice: TChoice;
|
|
9
9
|
balance: number;
|
|
10
10
|
scores: number[];
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export interface RankedChoiceVote {
|
|
20
|
-
choice: number[];
|
|
21
|
-
balance: number;
|
|
22
|
-
scores: number[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface QuadraticChoice {
|
|
26
|
-
[key: string]: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface QuadraticVote {
|
|
30
|
-
choice: QuadraticChoice;
|
|
31
|
-
balance: number;
|
|
32
|
-
scores: number[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface WeightedVote {
|
|
36
|
-
choice: { [key: string]: number };
|
|
37
|
-
balance: number;
|
|
38
|
-
scores: number[];
|
|
39
|
-
}
|
|
13
|
+
export type ChoiceMap = { [key: string]: number };
|
|
14
|
+
export type SingleChoiceVote = BaseVote<number>;
|
|
15
|
+
export type ApprovalVote = BaseVote<number[]>;
|
|
16
|
+
export type RankedChoiceVote = BaseVote<number[]>;
|
|
17
|
+
export type QuadraticVote = BaseVote<ChoiceMap>;
|
|
18
|
+
export type WeightedVote = BaseVote<ChoiceMap>;
|