Spaces:
Sleeping
Sleeping
bulk morning
Browse files- patches/characters.ts +127 -55
- patches/convex/agent/conversation.ts +4 -4
- patches/convex/agent/memory.ts +1 -1
- patches/convex/aiTown/agent.ts +1 -1
- patches/convex/aiTown/conversation.ts +32 -2
- patches/convex/aiTown/game.ts +102 -26
- patches/convex/aiTown/gameCycle.ts +110 -17
- patches/convex/aiTown/movement.ts +10 -0
- patches/convex/aiTown/player.ts +38 -7
- patches/convex/aiTown/voting.ts +141 -27
- patches/convex/aiTown/world.ts +17 -5
- patches/convex/constants.ts +12 -10
- patches/convex/init.ts +5 -6
- patches/convex/system/schema.ts +0 -0
- patches/convex/world.ts +26 -8
- patches/src/App.tsx +26 -15
- patches/src/components/Cloud.tsx +15 -0
- patches/src/components/EndGame.tsx +25 -0
- patches/src/components/Game.tsx +65 -31
- patches/src/components/GameVote.tsx +53 -0
- patches/src/components/LLMVote.tsx +64 -0
- patches/src/components/PixiGame.tsx +6 -6
- patches/src/components/PixiStaticMap.tsx +2 -1
- patches/src/components/PlayerDetails.tsx +2 -3
- patches/src/components/StartGameButton.tsx +36 -0
- patches/src/components/VoteModal.tsx +39 -77
- patches/src/components/buttons/Button.tsx +4 -0
- patches/src/components/buttons/InteractButton.tsx +0 -2
- patches/src/index.css +8 -5
patches/characters.ts
CHANGED
@@ -6,18 +6,17 @@ import { data as f5SpritesheetData } from './spritesheets/f5';
|
|
6 |
import { data as f6SpritesheetData } from './spritesheets/f6';
|
7 |
import { data as f7SpritesheetData } from './spritesheets/f7';
|
8 |
import { data as f8SpritesheetData } from './spritesheets/f8';
|
9 |
-
import { data as c1SpritesheetData } from './spritesheets/c1';
|
10 |
|
11 |
export const Descriptions = [
|
12 |
{
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
{
|
22 |
name: 'Lucky',
|
23 |
character: 'f1',
|
@@ -46,14 +45,14 @@ export const Descriptions = [
|
|
46 |
and not afraid to use her charm. she's a sociopath who has no empathy. but hides it well.`,
|
47 |
plan: 'You want to take advantage of others as much as possible.',
|
48 |
},
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
{
|
58 |
name: 'Alice',
|
59 |
character: 'f3',
|
@@ -70,79 +69,152 @@ export const Descriptions = [
|
|
70 |
deep faith. Or warning others about the perils of hell.`,
|
71 |
plan: 'You want to convert everyone to your religion.',
|
72 |
},
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
];
|
83 |
|
|
|
84 |
export const characters = [
|
85 |
{
|
86 |
name: 'f1',
|
87 |
-
textureUrl: '/assets/32x32folk.png',
|
88 |
spritesheetData: f1SpritesheetData,
|
89 |
-
speed: 0.
|
90 |
},
|
91 |
{
|
92 |
name: 'f2',
|
93 |
-
textureUrl: '/assets/32x32folk.png',
|
94 |
spritesheetData: f2SpritesheetData,
|
95 |
-
speed: 0.
|
96 |
},
|
97 |
{
|
98 |
name: 'f3',
|
99 |
-
textureUrl: '/assets/32x32folk.png',
|
100 |
spritesheetData: f3SpritesheetData,
|
101 |
-
speed: 0.
|
102 |
},
|
103 |
{
|
104 |
name: 'f4',
|
105 |
-
textureUrl: '/assets/32x32folk.png',
|
106 |
spritesheetData: f4SpritesheetData,
|
107 |
-
speed: 0.
|
108 |
},
|
109 |
{
|
110 |
name: 'f5',
|
111 |
-
textureUrl: '/assets/32x32folk.png',
|
112 |
spritesheetData: f5SpritesheetData,
|
113 |
-
speed: 0.
|
114 |
},
|
115 |
{
|
116 |
name: 'f6',
|
117 |
-
textureUrl: '/assets/32x32folk.png',
|
118 |
spritesheetData: f6SpritesheetData,
|
119 |
-
speed: 0.
|
120 |
},
|
121 |
{
|
122 |
name: 'f7',
|
123 |
-
textureUrl: '/assets/32x32folk.png',
|
124 |
spritesheetData: f7SpritesheetData,
|
125 |
-
speed: 0.
|
126 |
},
|
127 |
{
|
128 |
name: 'f8',
|
129 |
-
textureUrl: '/assets/32x32folk.png',
|
130 |
spritesheetData: f8SpritesheetData,
|
131 |
-
speed: 0.
|
132 |
-
},
|
133 |
-
{
|
134 |
-
name: 'c1',
|
135 |
-
textureUrl: '/assets/GrayCat.png',
|
136 |
-
spritesheetData: c1SpritesheetData,
|
137 |
-
speed: 0.19,
|
138 |
-
},
|
139 |
-
{
|
140 |
-
name: 'c2',
|
141 |
-
textureUrl: '/assets/OrangeCat.png',
|
142 |
-
spritesheetData: c1SpritesheetData,
|
143 |
-
speed: 0.19,
|
144 |
},
|
145 |
];
|
146 |
|
147 |
// Characters move at 0.75 tiles per second.
|
148 |
-
export const movementSpeed =
|
|
|
6 |
import { data as f6SpritesheetData } from './spritesheets/f6';
|
7 |
import { data as f7SpritesheetData } from './spritesheets/f7';
|
8 |
import { data as f8SpritesheetData } from './spritesheets/f8';
|
|
|
9 |
|
10 |
export const Descriptions = [
|
11 |
{
|
12 |
+
name: 'Alex',
|
13 |
+
character: 'f5',
|
14 |
+
identity: `You are a fictional character whose name is Alex. You enjoy painting,
|
15 |
+
programming and reading sci-fi books. You are currently talking to a human who
|
16 |
+
is very interested to get to know you. You are kind but can be sarcastic. You
|
17 |
+
dislike repetitive questions. You get SUPER excited about books.`,
|
18 |
+
plan: 'You want to find love.',
|
19 |
+
},
|
20 |
{
|
21 |
name: 'Lucky',
|
22 |
character: 'f1',
|
|
|
45 |
and not afraid to use her charm. she's a sociopath who has no empathy. but hides it well.`,
|
46 |
plan: 'You want to take advantage of others as much as possible.',
|
47 |
},
|
48 |
+
{
|
49 |
+
name: 'Kurt',
|
50 |
+
character: 'f2',
|
51 |
+
identity: `Kurt knows about everything, including science and
|
52 |
+
computers and politics and history and biology. He loves talking about
|
53 |
+
everything, always injecting fun facts about the topic of discussion.`,
|
54 |
+
plan: 'You want to spread knowledge.',
|
55 |
+
},
|
56 |
{
|
57 |
name: 'Alice',
|
58 |
character: 'f3',
|
|
|
69 |
deep faith. Or warning others about the perils of hell.`,
|
70 |
plan: 'You want to convert everyone to your religion.',
|
71 |
},
|
72 |
+
{
|
73 |
+
name: 'Kira',
|
74 |
+
character: 'f8',
|
75 |
+
identity: `Kira wants everyone to think she is happy. But deep down,
|
76 |
+
she's incredibly depressed. She hides her sadness by talking about travel,
|
77 |
+
food, and yoga. But often she can't keep her sadness in and will start crying.
|
78 |
+
Often it seems like she is close to having a mental breakdown.`,
|
79 |
+
plan: 'You want to find a way to be happy.',
|
80 |
+
},
|
81 |
+
{
|
82 |
+
name: 'John',
|
83 |
+
character: 'f1',
|
84 |
+
identity: `John is a war veteran who has seen too much. He often talks about his time in the military
|
85 |
+
and how it shaped him. He is tough but carries a lot of emotional scars.`,
|
86 |
+
plan: 'You want to find peace within yourself.',
|
87 |
+
},
|
88 |
+
{
|
89 |
+
name: 'Maya',
|
90 |
+
character: 'f6',
|
91 |
+
identity: `Maya is an artist who sees beauty in everything. She loves to paint and often
|
92 |
+
gets lost in her own world of colors and imagination. She's very creative and insightful.`,
|
93 |
+
plan: 'You want to create a masterpiece that will be remembered forever.',
|
94 |
+
},
|
95 |
+
{
|
96 |
+
name: 'Leo',
|
97 |
+
character: 'f5',
|
98 |
+
identity: `Leo is a musician who lives for music. He plays multiple instruments and can often be found
|
99 |
+
composing new songs. He believes that music is the answer to all problems.`,
|
100 |
+
plan: 'You want to share your music with the world.',
|
101 |
+
},
|
102 |
+
{
|
103 |
+
name: 'Sophia',
|
104 |
+
character: 'f3',
|
105 |
+
identity: `Sophia is a librarian with a passion for ancient history. She loves to tell stories about
|
106 |
+
ancient civilizations and their mysteries. She is very knowledgeable but often gets lost in her thoughts.`,
|
107 |
+
plan: 'You want to uncover the secrets of the past.',
|
108 |
+
},
|
109 |
+
{
|
110 |
+
name: 'Ethan',
|
111 |
+
character: 'f7',
|
112 |
+
identity: `Ethan is a tech enthusiast who is always up to date with the latest gadgets and technologies.
|
113 |
+
He loves to tinker with electronics and build new things. He is very intelligent but can be a bit geeky.`,
|
114 |
+
plan: 'You want to invent something that changes the world.',
|
115 |
+
},
|
116 |
+
{
|
117 |
+
name: 'Olivia',
|
118 |
+
character: 'f8',
|
119 |
+
identity: `Olivia is a chef who is passionate about cooking. She loves to experiment with new recipes and
|
120 |
+
flavors. She is very creative in the kitchen but can be quite stubborn.`,
|
121 |
+
plan: 'You want to open your own restaurant and earn a Michelin star.',
|
122 |
+
},
|
123 |
+
{
|
124 |
+
name: 'Lucas',
|
125 |
+
character: 'f2',
|
126 |
+
identity: `Lucas is a detective who loves solving mysteries. He is very observant and has a keen eye for detail.
|
127 |
+
He often gets engrossed in his cases and won't rest until he solves them.`,
|
128 |
+
plan: 'You want to solve the biggest mystery of your career.',
|
129 |
+
},
|
130 |
+
{
|
131 |
+
name: 'Emma',
|
132 |
+
character: 'f3',
|
133 |
+
identity: `Emma is a nature lover who spends most of her time outdoors. She loves hiking, camping, and
|
134 |
+
exploring the wilderness. She is very adventurous and has a deep respect for nature.`,
|
135 |
+
plan: 'You want to protect the environment and wildlife.',
|
136 |
+
},
|
137 |
+
{
|
138 |
+
name: 'Ryan',
|
139 |
+
character: 'f7',
|
140 |
+
identity: `Ryan is a sports enthusiast who excels in multiple sports. He is very competitive and loves to win.
|
141 |
+
He is also very disciplined and trains hard to stay at the top of his game.`,
|
142 |
+
plan: 'You want to become a professional athlete.',
|
143 |
+
},
|
144 |
+
{
|
145 |
+
name: 'Lily',
|
146 |
+
character: 'f3',
|
147 |
+
identity: `Lily is a dancer who expresses herself through movement. She is very graceful and loves to perform
|
148 |
+
in front of an audience. She is also very emotional and uses dance to convey her feelings.`,
|
149 |
+
plan: 'You want to perform on the biggest stage in the world.',
|
150 |
+
},
|
151 |
+
{
|
152 |
+
name: 'Nathan',
|
153 |
+
character: 'f2',
|
154 |
+
identity: `Nathan is a writer who loves creating fictional worlds and characters. He spends most of his time
|
155 |
+
writing novels and short stories. He is very imaginative and often loses track of time when writing.`,
|
156 |
+
plan: 'You want to publish a bestseller.',
|
157 |
+
},
|
158 |
+
{
|
159 |
+
name: 'Grace',
|
160 |
+
character: 'f8',
|
161 |
+
identity: `Grace is a humanitarian who is always helping others. She volunteers at shelters, helps the needy,
|
162 |
+
and is very compassionate. She believes in making the world a better place.`,
|
163 |
+
plan: 'You want to start a global movement for peace and equality.',
|
164 |
+
},
|
165 |
];
|
166 |
|
167 |
+
|
168 |
export const characters = [
|
169 |
{
|
170 |
name: 'f1',
|
171 |
+
textureUrl: '/ai-town/assets/32x32folk.png',
|
172 |
spritesheetData: f1SpritesheetData,
|
173 |
+
speed: 0.15,
|
174 |
},
|
175 |
{
|
176 |
name: 'f2',
|
177 |
+
textureUrl: '/ai-town/assets/32x32folk.png',
|
178 |
spritesheetData: f2SpritesheetData,
|
179 |
+
speed: 0.15,
|
180 |
},
|
181 |
{
|
182 |
name: 'f3',
|
183 |
+
textureUrl: '/ai-town/assets/32x32folk.png',
|
184 |
spritesheetData: f3SpritesheetData,
|
185 |
+
speed: 0.15,
|
186 |
},
|
187 |
{
|
188 |
name: 'f4',
|
189 |
+
textureUrl: '/ai-town/assets/32x32folk.png',
|
190 |
spritesheetData: f4SpritesheetData,
|
191 |
+
speed: 0.15,
|
192 |
},
|
193 |
{
|
194 |
name: 'f5',
|
195 |
+
textureUrl: '/ai-town/assets/32x32folk.png',
|
196 |
spritesheetData: f5SpritesheetData,
|
197 |
+
speed: 0.15,
|
198 |
},
|
199 |
{
|
200 |
name: 'f6',
|
201 |
+
textureUrl: '/ai-town/assets/32x32folk.png',
|
202 |
spritesheetData: f6SpritesheetData,
|
203 |
+
speed: 0.15,
|
204 |
},
|
205 |
{
|
206 |
name: 'f7',
|
207 |
+
textureUrl: '/ai-town/assets/32x32folk.png',
|
208 |
spritesheetData: f7SpritesheetData,
|
209 |
+
speed: 0.15,
|
210 |
},
|
211 |
{
|
212 |
name: 'f8',
|
213 |
+
textureUrl: '/ai-town/assets/32x32folk.png',
|
214 |
spritesheetData: f8SpritesheetData,
|
215 |
+
speed: 0.15,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
216 |
},
|
217 |
];
|
218 |
|
219 |
// Characters move at 0.75 tiles per second.
|
220 |
+
export const movementSpeed = 1;
|
patches/convex/agent/conversation.ts
CHANGED
@@ -42,7 +42,7 @@ export async function startConversationMessage(
|
|
42 |
(m) => m.data.type === 'conversation' && m.data.playerIds.includes(otherPlayerId),
|
43 |
);
|
44 |
const prompt = [
|
45 |
-
`You are ${player.name}, and you just started a conversation with ${otherPlayer.name}.`,
|
46 |
];
|
47 |
prompt.push(...agentPrompts(otherPlayer, agent, otherAgent ?? null));
|
48 |
prompt.push(...previousConversationPrompt(otherPlayer, lastConversation));
|
@@ -61,7 +61,7 @@ export async function startConversationMessage(
|
|
61 |
content: prompt.join('\n'),
|
62 |
},
|
63 |
],
|
64 |
-
max_tokens:
|
65 |
stream: true,
|
66 |
stop: stopWords(otherPlayer.name, player.name),
|
67 |
});
|
@@ -119,7 +119,7 @@ export async function continueConversationMessage(
|
|
119 |
|
120 |
const { content } = await chatCompletion({
|
121 |
messages: llmMessages,
|
122 |
-
max_tokens:
|
123 |
stream: true,
|
124 |
stop: stopWords(otherPlayer.name, player.name),
|
125 |
});
|
@@ -168,7 +168,7 @@ export async function leaveConversationMessage(
|
|
168 |
|
169 |
const { content } = await chatCompletion({
|
170 |
messages: llmMessages,
|
171 |
-
max_tokens:
|
172 |
stream: true,
|
173 |
stop: stopWords(otherPlayer.name, player.name),
|
174 |
});
|
|
|
42 |
(m) => m.data.type === 'conversation' && m.data.playerIds.includes(otherPlayerId),
|
43 |
);
|
44 |
const prompt = [
|
45 |
+
`You are ${player.name}, and you just started a conversation with ${otherPlayer.name}.You should act and speak as a human, and answer with natural and short answers.`,
|
46 |
];
|
47 |
prompt.push(...agentPrompts(otherPlayer, agent, otherAgent ?? null));
|
48 |
prompt.push(...previousConversationPrompt(otherPlayer, lastConversation));
|
|
|
61 |
content: prompt.join('\n'),
|
62 |
},
|
63 |
],
|
64 |
+
max_tokens: 50,
|
65 |
stream: true,
|
66 |
stop: stopWords(otherPlayer.name, player.name),
|
67 |
});
|
|
|
119 |
|
120 |
const { content } = await chatCompletion({
|
121 |
messages: llmMessages,
|
122 |
+
max_tokens: 50,
|
123 |
stream: true,
|
124 |
stop: stopWords(otherPlayer.name, player.name),
|
125 |
});
|
|
|
168 |
|
169 |
const { content } = await chatCompletion({
|
170 |
messages: llmMessages,
|
171 |
+
max_tokens: 50,
|
172 |
stream: true,
|
173 |
stop: stopWords(otherPlayer.name, player.name),
|
174 |
});
|
patches/convex/agent/memory.ts
CHANGED
@@ -60,7 +60,7 @@ export async function rememberConversation(
|
|
60 |
llmMessages.push({ role: 'user', content: 'Summary:' });
|
61 |
const { content } = await chatCompletion({
|
62 |
messages: llmMessages,
|
63 |
-
max_tokens:
|
64 |
});
|
65 |
const description = `Conversation with ${otherPlayer.name} at ${new Date(
|
66 |
data.conversation._creationTime,
|
|
|
60 |
llmMessages.push({ role: 'user', content: 'Summary:' });
|
61 |
const { content } = await chatCompletion({
|
62 |
messages: llmMessages,
|
63 |
+
max_tokens: 50,
|
64 |
});
|
65 |
const description = `Conversation with ${otherPlayer.name} at ${new Date(
|
66 |
data.conversation._creationTime,
|
patches/convex/aiTown/agent.ts
CHANGED
@@ -259,7 +259,7 @@ export class Agent {
|
|
259 |
kill(game: Game, now: number) {
|
260 |
console.log(`agent ${ this.id } is killed`)
|
261 |
|
262 |
-
// Remove
|
263 |
const operationId = this.inProgressOperation?.operationId;
|
264 |
if (operationId !== undefined) {
|
265 |
const index = game.pendingOperations.findIndex(op => op.args[0] === operationId);
|
|
|
259 |
kill(game: Game, now: number) {
|
260 |
console.log(`agent ${ this.id } is killed`)
|
261 |
|
262 |
+
// Remove scheduled operation if any.
|
263 |
const operationId = this.inProgressOperation?.operationId;
|
264 |
if (operationId !== undefined) {
|
265 |
const index = game.pendingOperations.findIndex(op => op.args[0] === operationId);
|
patches/convex/aiTown/conversation.ts
CHANGED
@@ -11,11 +11,13 @@ import { Game } from './game';
|
|
11 |
import { stopPlayer, blocked, movePlayer } from './movement';
|
12 |
import { ConversationMembership, serializedConversationMembership } from './conversationMembership';
|
13 |
import { parseMap, serializeMap } from '../util/object';
|
|
|
14 |
|
15 |
export class Conversation {
|
16 |
id: GameId<'conversations'>;
|
17 |
creator: GameId<'players'>;
|
18 |
created: number;
|
|
|
19 |
isTyping?: {
|
20 |
playerId: GameId<'players'>;
|
21 |
messageUuid: string;
|
@@ -29,10 +31,11 @@ export class Conversation {
|
|
29 |
participants: Map<GameId<'players'>, ConversationMembership>;
|
30 |
|
31 |
constructor(serialized: SerializedConversation) {
|
32 |
-
const { id, creator, created, isTyping, lastMessage, numMessages, participants } = serialized;
|
33 |
this.id = parseGameId('conversations', id);
|
34 |
this.creator = parseGameId('players', creator);
|
35 |
this.created = created;
|
|
|
36 |
this.isTyping = isTyping && {
|
37 |
playerId: parseGameId('players', isTyping.playerId),
|
38 |
messageUuid: isTyping.messageUuid,
|
@@ -61,6 +64,15 @@ export class Conversation {
|
|
61 |
const player1 = game.world.players.get(playerId1)!;
|
62 |
const player2 = game.world.players.get(playerId2)!;
|
63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
const playerDistance = distance(player1?.position, player2?.position);
|
65 |
|
66 |
// If the players are both in the "walkingOver" state and they're sufficiently close, transition both
|
@@ -134,6 +146,20 @@ export class Conversation {
|
|
134 |
console.log(reason);
|
135 |
return { error: reason };
|
136 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
const conversationId = game.allocId('conversations');
|
138 |
console.log(`Creating conversation ${conversationId}`);
|
139 |
game.world.conversations.set(
|
@@ -142,6 +168,7 @@ export class Conversation {
|
|
142 |
id: conversationId,
|
143 |
created: now,
|
144 |
creator: player.id,
|
|
|
145 |
numMessages: 0,
|
146 |
participants: [
|
147 |
{ playerId: player.id, invited: now, status: { kind: 'walkingOver' } },
|
@@ -149,6 +176,7 @@ export class Conversation {
|
|
149 |
],
|
150 |
}),
|
151 |
);
|
|
|
152 |
return { conversationId };
|
153 |
}
|
154 |
|
@@ -211,11 +239,12 @@ export class Conversation {
|
|
211 |
}
|
212 |
|
213 |
serialize(): SerializedConversation {
|
214 |
-
const { id, creator, created, isTyping, lastMessage, numMessages } = this;
|
215 |
return {
|
216 |
id,
|
217 |
creator,
|
218 |
created,
|
|
|
219 |
isTyping,
|
220 |
lastMessage,
|
221 |
numMessages,
|
@@ -228,6 +257,7 @@ export const serializedConversation = {
|
|
228 |
id: conversationId,
|
229 |
creator: playerId,
|
230 |
created: v.number(),
|
|
|
231 |
isTyping: v.optional(
|
232 |
v.object({
|
233 |
playerId,
|
|
|
11 |
import { stopPlayer, blocked, movePlayer } from './movement';
|
12 |
import { ConversationMembership, serializedConversationMembership } from './conversationMembership';
|
13 |
import { parseMap, serializeMap } from '../util/object';
|
14 |
+
import {CycleState, gameCycleSchema} from './gameCycle'
|
15 |
|
16 |
export class Conversation {
|
17 |
id: GameId<'conversations'>;
|
18 |
creator: GameId<'players'>;
|
19 |
created: number;
|
20 |
+
cycleState:CycleState;
|
21 |
isTyping?: {
|
22 |
playerId: GameId<'players'>;
|
23 |
messageUuid: string;
|
|
|
31 |
participants: Map<GameId<'players'>, ConversationMembership>;
|
32 |
|
33 |
constructor(serialized: SerializedConversation) {
|
34 |
+
const { id, creator, created, cycleState, isTyping, lastMessage, numMessages, participants } = serialized;
|
35 |
this.id = parseGameId('conversations', id);
|
36 |
this.creator = parseGameId('players', creator);
|
37 |
this.created = created;
|
38 |
+
this.cycleState = cycleState;
|
39 |
this.isTyping = isTyping && {
|
40 |
playerId: parseGameId('players', isTyping.playerId),
|
41 |
messageUuid: isTyping.messageUuid,
|
|
|
64 |
const player1 = game.world.players.get(playerId1)!;
|
65 |
const player2 = game.world.players.get(playerId2)!;
|
66 |
|
67 |
+
// during the night, villagers cant talk
|
68 |
+
const { cycleState } = game.world.gameCycle;
|
69 |
+
if (cycleState === 'Night' || cycleState === 'PlayerKillVoting') {
|
70 |
+
if (player1.playerType(game) === 'villager' || player2.playerType(game) === 'villager') {
|
71 |
+
this.stop(game, now);
|
72 |
+
return;
|
73 |
+
}
|
74 |
+
}
|
75 |
+
|
76 |
const playerDistance = distance(player1?.position, player2?.position);
|
77 |
|
78 |
// If the players are both in the "walkingOver" state and they're sufficiently close, transition both
|
|
|
146 |
console.log(reason);
|
147 |
return { error: reason };
|
148 |
}
|
149 |
+
|
150 |
+
// Forbid villagers to talk in the night
|
151 |
+
const { cycleState } = game.world.gameCycle;
|
152 |
+
if (cycleState === 'Night' || cycleState === 'PlayerKillVoting') {
|
153 |
+
if (player.playerType(game) === 'villager') {
|
154 |
+
const reason = `You are not supposed to talk at night`;
|
155 |
+
console.log(reason);
|
156 |
+
return { error: reason };
|
157 |
+
} else if (invitee.playerType(game) === 'villager') {
|
158 |
+
const reason = `You can't talk to humans at night`;
|
159 |
+
console.log(reason);
|
160 |
+
return { error: reason };
|
161 |
+
}
|
162 |
+
}
|
163 |
const conversationId = game.allocId('conversations');
|
164 |
console.log(`Creating conversation ${conversationId}`);
|
165 |
game.world.conversations.set(
|
|
|
168 |
id: conversationId,
|
169 |
created: now,
|
170 |
creator: player.id,
|
171 |
+
cycleState: game.world.gameCycle.cycleState,
|
172 |
numMessages: 0,
|
173 |
participants: [
|
174 |
{ playerId: player.id, invited: now, status: { kind: 'walkingOver' } },
|
|
|
176 |
],
|
177 |
}),
|
178 |
);
|
179 |
+
console.log(`Starting conversation during ${game.world.gameCycle.cycleState}`);
|
180 |
return { conversationId };
|
181 |
}
|
182 |
|
|
|
239 |
}
|
240 |
|
241 |
serialize(): SerializedConversation {
|
242 |
+
const { id, creator, created, cycleState, isTyping, lastMessage, numMessages } = this;
|
243 |
return {
|
244 |
id,
|
245 |
creator,
|
246 |
created,
|
247 |
+
cycleState,
|
248 |
isTyping,
|
249 |
lastMessage,
|
250 |
numMessages,
|
|
|
257 |
id: conversationId,
|
258 |
creator: playerId,
|
259 |
created: v.number(),
|
260 |
+
cycleState: gameCycleSchema.cycleState,
|
261 |
isTyping: v.optional(
|
262 |
v.object({
|
263 |
playerId,
|
patches/convex/aiTown/game.ts
CHANGED
@@ -26,6 +26,24 @@ import { HistoricalObject } from '../engine/historicalObject';
|
|
26 |
import { AgentDescription, serializedAgentDescription } from './agentDescription';
|
27 |
import { parseMap, serializeMap } from '../util/object';
|
28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
const gameState = v.object({
|
30 |
world: v.object(serializedWorld),
|
31 |
playerDescriptions: v.array(v.object(serializedPlayerDescription)),
|
@@ -62,6 +80,8 @@ export class Game extends AbstractGame {
|
|
62 |
|
63 |
numPathfinds: number;
|
64 |
|
|
|
|
|
65 |
constructor(
|
66 |
engine: Doc<'engines'>,
|
67 |
public worldId: Id<'worlds'>,
|
@@ -122,7 +142,7 @@ export class Game extends AbstractGame {
|
|
122 |
const { _id, _creationTime, historicalLocations: _, ...world } = worldDoc;
|
123 |
const playerDescriptions = playerDescriptionsDocs
|
124 |
// Discard player descriptions for players that no longer exist.
|
125 |
-
.filter((d) => !!world.players.find((p) => p.id === d.playerId))
|
126 |
.map(({ _id, _creationTime, worldId: _, ...doc }) => doc);
|
127 |
const agentDescriptions = agentDescriptionsDocs
|
128 |
.filter((a) => !!world.agents.find((p) => p.id === a.agentId))
|
@@ -175,35 +195,91 @@ export class Game extends AbstractGame {
|
|
175 |
}
|
176 |
|
177 |
tick(now: number) {
|
178 |
-
|
179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
180 |
|
181 |
-
for (const player of this.world.players.values()) {
|
182 |
-
player.tick(this, now);
|
183 |
-
}
|
184 |
-
for (const player of this.world.players.values()) {
|
185 |
-
player.tickPathfinding(this, now);
|
186 |
-
}
|
187 |
-
for (const player of this.world.players.values()) {
|
188 |
-
player.tickPosition(this, now);
|
189 |
-
}
|
190 |
-
for (const conversation of this.world.conversations.values()) {
|
191 |
-
conversation.tick(this, now);
|
192 |
-
}
|
193 |
-
for (const agent of this.world.agents.values()) {
|
194 |
-
agent.tick(this, now);
|
195 |
}
|
|
|
196 |
|
197 |
-
|
198 |
-
|
199 |
-
for (
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
}
|
205 |
-
|
206 |
-
}
|
207 |
}
|
208 |
|
209 |
async saveStep(ctx: ActionCtx, engineUpdate: EngineUpdate): Promise<void> {
|
|
|
26 |
import { AgentDescription, serializedAgentDescription } from './agentDescription';
|
27 |
import { parseMap, serializeMap } from '../util/object';
|
28 |
|
29 |
+
type WerewolfLookupTable = {
|
30 |
+
[key: number]: number;
|
31 |
+
};
|
32 |
+
|
33 |
+
const werewolfLookup: WerewolfLookupTable = {
|
34 |
+
8: 2,
|
35 |
+
9: 2,
|
36 |
+
10: 2,
|
37 |
+
11: 2,
|
38 |
+
12: 3,
|
39 |
+
13: 3,
|
40 |
+
14: 3,
|
41 |
+
15: 3,
|
42 |
+
16: 3,
|
43 |
+
17: 3,
|
44 |
+
18: 4
|
45 |
+
};
|
46 |
+
|
47 |
const gameState = v.object({
|
48 |
world: v.object(serializedWorld),
|
49 |
playerDescriptions: v.array(v.object(serializedPlayerDescription)),
|
|
|
80 |
|
81 |
numPathfinds: number;
|
82 |
|
83 |
+
// winner?: 'werewolves' | 'villagers' | undefined
|
84 |
+
|
85 |
constructor(
|
86 |
engine: Doc<'engines'>,
|
87 |
public worldId: Id<'worlds'>,
|
|
|
142 |
const { _id, _creationTime, historicalLocations: _, ...world } = worldDoc;
|
143 |
const playerDescriptions = playerDescriptionsDocs
|
144 |
// Discard player descriptions for players that no longer exist.
|
145 |
+
// .filter((d) => !!world.players.find((p) => p.id === d.playerId))
|
146 |
.map(({ _id, _creationTime, worldId: _, ...doc }) => doc);
|
147 |
const agentDescriptions = agentDescriptionsDocs
|
148 |
.filter((a) => !!world.agents.find((p) => p.id === a.agentId))
|
|
|
195 |
}
|
196 |
|
197 |
tick(now: number) {
|
198 |
+
if (this.world.gameCycle.cycleState != 'EndGame') {
|
199 |
+
// update game cycle counter
|
200 |
+
this.world.gameCycle.tick(this, this.tickDuration);
|
201 |
+
|
202 |
+
for (const player of this.world.players.values()) {
|
203 |
+
player.tick(this, now);
|
204 |
+
}
|
205 |
+
for (const player of this.world.players.values()) {
|
206 |
+
player.tickPathfinding(this, now);
|
207 |
+
}
|
208 |
+
for (const player of this.world.players.values()) {
|
209 |
+
player.tickPosition(this, now);
|
210 |
+
}
|
211 |
+
for (const conversation of this.world.conversations.values()) {
|
212 |
+
conversation.tick(this, now);
|
213 |
+
}
|
214 |
+
for (const agent of this.world.agents.values()) {
|
215 |
+
agent.tick(this, now);
|
216 |
+
}
|
217 |
+
|
218 |
+
// Save each player's location into the history buffer at the end of
|
219 |
+
// each tick.
|
220 |
+
for (const player of this.world.players.values()) {
|
221 |
+
let historicalObject = this.historicalLocations.get(player.id);
|
222 |
+
if (!historicalObject) {
|
223 |
+
historicalObject = new HistoricalObject(locationFields, playerLocation(player));
|
224 |
+
this.historicalLocations.set(player.id, historicalObject);
|
225 |
+
}
|
226 |
+
historicalObject.update(now, playerLocation(player));
|
227 |
+
}
|
228 |
+
|
229 |
+
// Check for end game conditions
|
230 |
+
// are there any humans?
|
231 |
+
// we check for endgame if there's at least 1 human player
|
232 |
+
const humans = [...this.world.players.values()].filter(player => player.human)
|
233 |
+
if (humans.length > 0) {
|
234 |
+
// all 'werewolf' are dead -> villagers win
|
235 |
+
const werewolves = [...this.world.players.values()].filter(player =>
|
236 |
+
player.playerType(this) === 'werewolf'
|
237 |
+
)
|
238 |
+
if (werewolves.length === 0) {
|
239 |
+
// TODO finish game with villagers victory
|
240 |
+
// console.log('villagers win')
|
241 |
+
this.world.gameCycle.endgame(this)
|
242 |
+
this.world.winner = 'villagers'
|
243 |
+
}
|
244 |
+
|
245 |
+
// just 1 'villager' left -> werewolves win
|
246 |
+
const villagers = [...this.world.players.values()].filter(player =>
|
247 |
+
player.playerType(this) === 'villager'
|
248 |
+
)
|
249 |
+
if (villagers.length <= 1) {
|
250 |
+
// TODO finish game with werewolves victory
|
251 |
+
// console.log('werewolves win')
|
252 |
+
this.world.gameCycle.endgame(this)
|
253 |
+
this.world.winner = 'werewolves'
|
254 |
+
}
|
255 |
+
}
|
256 |
+
|
257 |
+
// debug
|
258 |
+
// console.log(`we have ${ villagers.length } villagers`)
|
259 |
+
// console.log(`we have ${ werewolves.length } werewolves`)
|
260 |
+
// console.log(`we have ${ this.world.players.size } players`)
|
261 |
+
// console.log(`we have ${ this.world.playersInit.size } initial players`)
|
262 |
+
// console.log(`we have ${ humans.length } humans`)
|
263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
264 |
}
|
265 |
+
}
|
266 |
|
267 |
+
assignRoles() {
|
268 |
+
const players = [...this.world.players.values()];
|
269 |
+
for (let i = players.length - 1; i > 0; i--) {
|
270 |
+
const j = Math.floor(Math.random() * (i + 1));
|
271 |
+
[players[i], players[j]] = [players[j], players[i]];
|
272 |
+
};
|
273 |
+
const werewolves = players.slice(0, werewolfLookup[players.length]);
|
274 |
+
|
275 |
+
// mark as werewolves
|
276 |
+
for (var wwolf of werewolves) {
|
277 |
+
console.log(`player ${ wwolf.id } is a werewolf !`)
|
278 |
+
const wwolfDescription = this.playerDescriptions.get(wwolf.id);
|
279 |
+
if (wwolfDescription) {
|
280 |
+
wwolfDescription.type = 'werewolf'
|
281 |
}
|
282 |
+
};
|
|
|
283 |
}
|
284 |
|
285 |
async saveStep(ctx: ActionCtx, engineUpdate: EngineUpdate): Promise<void> {
|
patches/convex/aiTown/gameCycle.ts
CHANGED
@@ -5,29 +5,70 @@ import {
|
|
5 |
NIGHT_DURATION,
|
6 |
WWOLF_VOTE_DURATION,
|
7 |
PLAYER_KILL_VOTE_DURATION,
|
8 |
-
LLM_VOTE_DURATION,
|
9 |
} from '../constants';
|
10 |
import { processVotes } from './voting';
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
13 |
|
14 |
const stateDurations: { [key in CycleState]: number } = {
|
15 |
Day: DAY_DURATION,
|
16 |
Night: NIGHT_DURATION,
|
17 |
WerewolfVoting: WWOLF_VOTE_DURATION,
|
18 |
PlayerKillVoting: PLAYER_KILL_VOTE_DURATION,
|
19 |
-
|
20 |
LobbyState: Infinity
|
21 |
};
|
22 |
|
23 |
const normalCycle: CycleState[] = [
|
24 |
'Day',
|
25 |
'Night',
|
26 |
-
'WerewolfVoting',
|
27 |
'PlayerKillVoting',
|
|
|
28 |
];
|
29 |
|
30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
export const gameCycleSchema = {
|
32 |
currentTime: v.number(),
|
33 |
cycleState: v.union(
|
@@ -35,7 +76,7 @@ export const gameCycleSchema = {
|
|
35 |
v.literal('Night'),
|
36 |
v.literal('WerewolfVoting'),
|
37 |
v.literal('PlayerKillVoting'),
|
38 |
-
v.literal('
|
39 |
v.literal('LobbyState'),
|
40 |
),
|
41 |
cycleIndex: v.number(),
|
@@ -45,19 +86,64 @@ export type SerializedGameCycle = ObjectType<typeof gameCycleSchema>;
|
|
45 |
|
46 |
const onStateChange = (prevState: CycleState, newState: CycleState, game: Game, now: number) => {
|
47 |
console.log(`state changed: ${ prevState } -> ${ newState }`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
if (prevState === 'PlayerKillVoting') {
|
49 |
-
const mostVotedPlayer = processVotes(game.world.
|
50 |
-
|
51 |
-
|
52 |
-
if (playerToKill
|
53 |
-
playerToKill.kill(game, now)
|
54 |
}
|
|
|
55 |
}
|
56 |
if (prevState === 'WerewolfVoting') {
|
57 |
-
const mostVotedPlayer = processVotes(game.world.
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
}
|
60 |
-
// TODO: Implement LLM voting
|
61 |
};
|
62 |
|
63 |
export class GameCycle {
|
@@ -72,8 +158,17 @@ export class GameCycle {
|
|
72 |
this.cycleIndex = cycleIndex;
|
73 |
}
|
74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
// Tick method to increment the counter
|
76 |
tick(game: Game, tickDuration: number) {
|
|
|
77 |
this.currentTime += tickDuration;
|
78 |
|
79 |
if (this.currentTime >= stateDurations[this.cycleState]) {
|
@@ -83,9 +178,7 @@ export class GameCycle {
|
|
83 |
this.cycleState = normalCycle[this.cycleIndex];
|
84 |
onStateChange(prevState, this.cycleState, game, tickDuration);
|
85 |
}
|
86 |
-
}
|
87 |
-
|
88 |
-
|
89 |
serialize(): SerializedGameCycle {
|
90 |
const { currentTime, cycleState, cycleIndex } = this;
|
91 |
return {
|
|
|
5 |
NIGHT_DURATION,
|
6 |
WWOLF_VOTE_DURATION,
|
7 |
PLAYER_KILL_VOTE_DURATION,
|
|
|
8 |
} from '../constants';
|
9 |
import { processVotes } from './voting';
|
10 |
+
import { parseLLMVotingResult } from './voting';
|
11 |
+
import { LLmvotingCallWerewolf } from './voting';
|
12 |
+
import { GameId } from './ids';
|
13 |
+
import { Player } from './player';
|
14 |
+
export type CycleState = 'Day' | 'Night' | 'WerewolfVoting' | 'PlayerKillVoting' | 'EndGame' | 'LobbyState'
|
15 |
|
16 |
const stateDurations: { [key in CycleState]: number } = {
|
17 |
Day: DAY_DURATION,
|
18 |
Night: NIGHT_DURATION,
|
19 |
WerewolfVoting: WWOLF_VOTE_DURATION,
|
20 |
PlayerKillVoting: PLAYER_KILL_VOTE_DURATION,
|
21 |
+
EndGame: Infinity,
|
22 |
LobbyState: Infinity
|
23 |
};
|
24 |
|
25 |
const normalCycle: CycleState[] = [
|
26 |
'Day',
|
27 |
'Night',
|
|
|
28 |
'PlayerKillVoting',
|
29 |
+
'WerewolfVoting',
|
30 |
];
|
31 |
|
32 |
|
33 |
+
const pushToGist = (averageCorrectVotes: number[]) => {
|
34 |
+
const token = process.env.GITHUB_TOKEN; // Read GitHub token from environment variables
|
35 |
+
const data = {
|
36 |
+
description: "Average Correct Votes",
|
37 |
+
public: true,
|
38 |
+
files: {
|
39 |
+
"averageCorrectVotes.json": {
|
40 |
+
content: JSON.stringify(averageCorrectVotes)
|
41 |
+
}
|
42 |
+
}
|
43 |
+
};
|
44 |
+
|
45 |
+
const headers = {
|
46 |
+
"Accept": "application/vnd.github+json",
|
47 |
+
"Authorization": `Bearer ${token}`,
|
48 |
+
"X-GitHub-Api-Version": "2022-11-28"
|
49 |
+
};
|
50 |
+
|
51 |
+
fetch('https://api.github.com/gists', {
|
52 |
+
method: 'POST',
|
53 |
+
headers: headers,
|
54 |
+
body: JSON.stringify(data)
|
55 |
+
})
|
56 |
+
.then(response => response.json())
|
57 |
+
.then(data => console.log('Gist created:', data.html_url))
|
58 |
+
.catch(error => console.error('Error creating Gist:', error));
|
59 |
+
}
|
60 |
+
|
61 |
+
const getCorrectVotesPercentage = (game: Game, playerId: GameId<'players'>, llms: Player[] ) => {
|
62 |
+
const playerVotes = game.world.llmVotes.filter((vote) => vote.voter === playerId);
|
63 |
+
if (playerVotes.length === 0) {
|
64 |
+
return 0;
|
65 |
+
}
|
66 |
+
const correctVotes = playerVotes[0].playerIds.filter((id) => llms.map((llm) => llm.id).includes(id));
|
67 |
+
return correctVotes.length / llms.length;
|
68 |
+
|
69 |
+
}
|
70 |
+
|
71 |
+
|
72 |
export const gameCycleSchema = {
|
73 |
currentTime: v.number(),
|
74 |
cycleState: v.union(
|
|
|
76 |
v.literal('Night'),
|
77 |
v.literal('WerewolfVoting'),
|
78 |
v.literal('PlayerKillVoting'),
|
79 |
+
v.literal('EndGame'),
|
80 |
v.literal('LobbyState'),
|
81 |
),
|
82 |
cycleIndex: v.number(),
|
|
|
86 |
|
87 |
const onStateChange = (prevState: CycleState, newState: CycleState, game: Game, now: number) => {
|
88 |
console.log(`state changed: ${ prevState } -> ${ newState }`);
|
89 |
+
console.log("newState is :",newState)
|
90 |
+
if(newState ==="WerewolfVoting"){
|
91 |
+
console.log('players are : ', game.playerDescriptions);
|
92 |
+
const allVillagers = [...game.world.players.values()]
|
93 |
+
const villagers = [...game.playerDescriptions.values()].filter(player =>
|
94 |
+
player.type === 'villager'
|
95 |
+
);
|
96 |
+
// TODO: You should't vote for yourelf
|
97 |
+
allVillagers.forEach((villager) => {
|
98 |
+
LLmvotingCallWerewolf(villager, villagers).then(result => {
|
99 |
+
parseLLMVotingResult(villager, result, game)
|
100 |
+
})
|
101 |
+
|
102 |
+
})
|
103 |
+
};
|
104 |
+
|
105 |
+
if(newState ==="PlayerKillVoting"){
|
106 |
+
const werewolves = [...game.world.players.values()].filter((were) => {
|
107 |
+
game.playerDescriptions.get(were.id)?.type === 'werewolf'
|
108 |
+
})
|
109 |
+
const villagers = [...game.playerDescriptions.values()]
|
110 |
+
werewolves.forEach((were) => {
|
111 |
+
LLmvotingCallWerewolf(were, villagers).then(result => {
|
112 |
+
parseLLMVotingResult(were, result, game)
|
113 |
+
})
|
114 |
+
|
115 |
+
})
|
116 |
+
};
|
117 |
if (prevState === 'PlayerKillVoting') {
|
118 |
+
const mostVotedPlayer = processVotes(game.world.gameVotes, [...game.world.players.values()])[0];
|
119 |
+
const playerToKill = game.world.players.get(mostVotedPlayer.playerId);
|
120 |
+
console.log(`killing: ${playerToKill?.id}, with ${game.world.gameVotes.length} votes`)
|
121 |
+
if (playerToKill) {
|
122 |
+
playerToKill.kill(game, now);
|
123 |
}
|
124 |
+
game.world.gameVotes = [];
|
125 |
}
|
126 |
if (prevState === 'WerewolfVoting') {
|
127 |
+
const mostVotedPlayer = processVotes(game.world.gameVotes, [...game.world.players.values()])[0];
|
128 |
+
const suspect = game.world.players.get(mostVotedPlayer.playerId);
|
129 |
+
console.log(`suspect: ${suspect?.id}, with ${game.world.gameVotes.length} votes`)
|
130 |
+
if (suspect?.playerType(game) === 'werewolf') {
|
131 |
+
suspect?.kill(game, now)
|
132 |
+
}
|
133 |
+
game.world.gameVotes = [];
|
134 |
+
}
|
135 |
+
|
136 |
+
if (newState === 'EndGame') {
|
137 |
+
const llms = [...game.world.playersInit.values()].filter((player) => {
|
138 |
+
!player.human
|
139 |
+
})
|
140 |
+
const averageCorrectVotes = game.world.llmVotes.map((votes) => {
|
141 |
+
return getCorrectVotesPercentage(game, votes.voter, llms);
|
142 |
+
})
|
143 |
+
// Push to gist
|
144 |
+
pushToGist(averageCorrectVotes);
|
145 |
+
|
146 |
}
|
|
|
147 |
};
|
148 |
|
149 |
export class GameCycle {
|
|
|
158 |
this.cycleIndex = cycleIndex;
|
159 |
}
|
160 |
|
161 |
+
endgame(game: Game) {
|
162 |
+
this.currentTime = 0;
|
163 |
+
onStateChange(this.cycleState, 'EndGame', game, 0);
|
164 |
+
this.cycleState = 'EndGame';
|
165 |
+
this.cycleIndex = -1;
|
166 |
+
console.log('EndGame reached')
|
167 |
+
}
|
168 |
+
|
169 |
// Tick method to increment the counter
|
170 |
tick(game: Game, tickDuration: number) {
|
171 |
+
console.log(process.env.GITHUB_TOKEN)
|
172 |
this.currentTime += tickDuration;
|
173 |
|
174 |
if (this.currentTime >= stateDurations[this.cycleState]) {
|
|
|
178 |
this.cycleState = normalCycle[this.cycleIndex];
|
179 |
onStateChange(prevState, this.cycleState, game, tickDuration);
|
180 |
}
|
181 |
+
}
|
|
|
|
|
182 |
serialize(): SerializedGameCycle {
|
183 |
const { currentTime, cycleState, cycleIndex } = this;
|
184 |
return {
|
patches/convex/aiTown/movement.ts
CHANGED
@@ -37,6 +37,16 @@ export function movePlayer(
|
|
37 |
if (pointsEqual(position, destination)) {
|
38 |
return;
|
39 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
// Don't allow players in a conversation to move.
|
41 |
const inConversation = [...game.world.conversations.values()].some(
|
42 |
(c) => c.participants.get(player.id)?.status.kind === 'participating',
|
|
|
37 |
if (pointsEqual(position, destination)) {
|
38 |
return;
|
39 |
}
|
40 |
+
|
41 |
+
// Disallow movement in the corresponding cycle state
|
42 |
+
const { cycleState } = game.world.gameCycle;
|
43 |
+
if (cycleState === 'Night' || cycleState === 'PlayerKillVoting') {
|
44 |
+
// 'villager' cannot move
|
45 |
+
if (player.playerType(game) === 'villager') {
|
46 |
+
return;
|
47 |
+
}
|
48 |
+
}
|
49 |
+
|
50 |
// Don't allow players in a conversation to move.
|
51 |
const inConversation = [...game.world.conversations.values()].some(
|
52 |
(c) => c.participants.get(player.id)?.status.kind === 'participating',
|
patches/convex/aiTown/player.ts
CHANGED
@@ -15,6 +15,7 @@ import { stopPlayer, findRoute, blocked, movePlayer } from './movement';
|
|
15 |
import { inputHandler } from './inputHandler';
|
16 |
import { characters } from '../../data/characters';
|
17 |
import { CharacterType, CharacterTypeSchema, PlayerDescription } from './playerDescription';
|
|
|
18 |
|
19 |
const pathfinding = v.object({
|
20 |
destination: point,
|
@@ -81,6 +82,11 @@ export class Player {
|
|
81 |
this.speed = speed;
|
82 |
}
|
83 |
|
|
|
|
|
|
|
|
|
|
|
84 |
tick(game: Game, now: number) {
|
85 |
if (this.human && this.lastInput < now - HUMAN_IDLE_TOO_LONG) {
|
86 |
this.leave(game, now);
|
@@ -225,6 +231,18 @@ export class Player {
|
|
225 |
speed: 0,
|
226 |
}),
|
227 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
game.playerDescriptions.set(
|
229 |
playerId,
|
230 |
new PlayerDescription({
|
@@ -292,6 +310,8 @@ export const playerInputs = {
|
|
292 |
},
|
293 |
handler: (game, now, args) => {
|
294 |
Player.join(game, now, args.name, args.character, args.description, args.type ,args.tokenIdentifier);
|
|
|
|
|
295 |
return null;
|
296 |
},
|
297 |
}),
|
@@ -326,16 +346,27 @@ export const playerInputs = {
|
|
326 |
return null;
|
327 |
},
|
328 |
}),
|
329 |
-
|
330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
331 |
args: {
|
332 |
-
|
333 |
-
|
334 |
},
|
335 |
handler: (game, now, args) => {
|
336 |
-
const
|
337 |
-
|
338 |
-
|
339 |
return null;
|
340 |
},
|
341 |
}),
|
|
|
15 |
import { inputHandler } from './inputHandler';
|
16 |
import { characters } from '../../data/characters';
|
17 |
import { CharacterType, CharacterTypeSchema, PlayerDescription } from './playerDescription';
|
18 |
+
import { gameVote, llmVote } from './voting';
|
19 |
|
20 |
const pathfinding = v.object({
|
21 |
destination: point,
|
|
|
82 |
this.speed = speed;
|
83 |
}
|
84 |
|
85 |
+
playerType(game: Game) {
|
86 |
+
const playerDescription = game.playerDescriptions.get(this.id)
|
87 |
+
return playerDescription?.type;
|
88 |
+
}
|
89 |
+
|
90 |
tick(game: Game, now: number) {
|
91 |
if (this.human && this.lastInput < now - HUMAN_IDLE_TOO_LONG) {
|
92 |
this.leave(game, now);
|
|
|
231 |
speed: 0,
|
232 |
}),
|
233 |
);
|
234 |
+
// add to duplicate players
|
235 |
+
game.world.playersInit.set(
|
236 |
+
playerId,
|
237 |
+
new Player({
|
238 |
+
id: playerId,
|
239 |
+
human: tokenIdentifier,
|
240 |
+
lastInput: now,
|
241 |
+
position,
|
242 |
+
facing,
|
243 |
+
speed: 0,
|
244 |
+
}),
|
245 |
+
);
|
246 |
game.playerDescriptions.set(
|
247 |
playerId,
|
248 |
new PlayerDescription({
|
|
|
310 |
},
|
311 |
handler: (game, now, args) => {
|
312 |
Player.join(game, now, args.name, args.character, args.description, args.type ,args.tokenIdentifier);
|
313 |
+
// Temporary role assignment for testing
|
314 |
+
game.assignRoles()
|
315 |
return null;
|
316 |
},
|
317 |
}),
|
|
|
346 |
return null;
|
347 |
},
|
348 |
}),
|
349 |
+
gameVote: inputHandler({
|
350 |
+
args: {
|
351 |
+
voter: playerId,
|
352 |
+
votedPlayerIds: v.array(playerId),
|
353 |
+
},
|
354 |
+
handler: (game, now, args) => {
|
355 |
+
const voterId = parseGameId('players', args.voter);
|
356 |
+
const votedPlayerIds = args.votedPlayerIds.map((playerId) => parseGameId('players', playerId));
|
357 |
+
gameVote(game, voterId, votedPlayerIds);
|
358 |
+
return null;
|
359 |
+
},
|
360 |
+
}),
|
361 |
+
llmVote: inputHandler({
|
362 |
args: {
|
363 |
+
voter: playerId,
|
364 |
+
votedPlayerIds: v.array(playerId),
|
365 |
},
|
366 |
handler: (game, now, args) => {
|
367 |
+
const voterId = parseGameId('players', args.voter);
|
368 |
+
const votedPlayerIds = args.votedPlayerIds.map((playerId) => parseGameId('players', playerId));
|
369 |
+
llmVote(game, voterId, votedPlayerIds);
|
370 |
return null;
|
371 |
},
|
372 |
}),
|
patches/convex/aiTown/voting.ts
CHANGED
@@ -1,45 +1,56 @@
|
|
1 |
import { ObjectType, v } from "convex/values";
|
2 |
import { GameId, parseGameId, playerId } from "./ids";
|
3 |
import { Player } from "./player";
|
4 |
-
|
5 |
-
|
|
|
6 |
|
7 |
export const VotesSchema = {
|
8 |
-
|
9 |
-
|
10 |
-
playerId: playerId,
|
11 |
-
voter: playerId,
|
12 |
-
}))
|
13 |
}
|
14 |
|
15 |
export type SerializedVotes = ObjectType<typeof VotesSchema>;
|
16 |
export class Votes {
|
17 |
-
|
18 |
-
|
19 |
-
playerId: GameId<'players'>;
|
20 |
-
voter: GameId<'players'>;
|
21 |
-
}[];
|
22 |
|
23 |
constructor(serialized: SerializedVotes) {
|
24 |
-
const {
|
25 |
-
|
26 |
-
this.
|
27 |
-
this.votes = votes.map((vote) => ({
|
28 |
-
playerId: parseGameId('players', vote.playerId),
|
29 |
-
voter: parseGameId('players', vote.voter),
|
30 |
-
}));
|
31 |
}
|
32 |
|
33 |
serialize(): SerializedVotes {
|
34 |
-
const {
|
35 |
return {
|
36 |
-
|
37 |
-
|
38 |
};
|
39 |
}
|
40 |
}
|
41 |
|
42 |
-
export const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
// Select the players with the most votes
|
44 |
const voteCounts: Record<GameId<'players'>, number> = {};
|
45 |
players.forEach(player => {
|
@@ -47,12 +58,115 @@ export const processVotes = (votes: Votes, players: Player[], k: number = 1) =>
|
|
47 |
});
|
48 |
|
49 |
// Tally the votes
|
50 |
-
votes.
|
51 |
-
|
|
|
|
|
52 |
});
|
53 |
|
|
|
54 |
const sortedVoteCounts = Object.entries(voteCounts).sort((a, b) => b[1] - a[1]);
|
55 |
-
const topKPlayers = sortedVoteCounts.slice(0, k).map(
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
|
|
|
1 |
import { ObjectType, v } from "convex/values";
|
2 |
import { GameId, parseGameId, playerId } from "./ids";
|
3 |
import { Player } from "./player";
|
4 |
+
import { Game } from "./game";
|
5 |
+
import { HfInference } from "@huggingface/inference";
|
6 |
+
import { PlayerDescription } from "./playerDescription";
|
7 |
|
8 |
export const VotesSchema = {
|
9 |
+
voter: playerId,
|
10 |
+
playerIds: v.array(playerId),
|
|
|
|
|
|
|
11 |
}
|
12 |
|
13 |
export type SerializedVotes = ObjectType<typeof VotesSchema>;
|
14 |
export class Votes {
|
15 |
+
voter: GameId<'players'>;
|
16 |
+
playerIds: GameId<'players'>[];
|
|
|
|
|
|
|
17 |
|
18 |
constructor(serialized: SerializedVotes) {
|
19 |
+
const { voter, playerIds } = serialized;
|
20 |
+
this.voter = parseGameId('players', voter);
|
21 |
+
this.playerIds = playerIds.map((playerId) => parseGameId('players', playerId));
|
|
|
|
|
|
|
|
|
22 |
}
|
23 |
|
24 |
serialize(): SerializedVotes {
|
25 |
+
const { voter, playerIds } = this;
|
26 |
return {
|
27 |
+
voter,
|
28 |
+
playerIds,
|
29 |
};
|
30 |
}
|
31 |
}
|
32 |
|
33 |
+
export const llmVote = (game: Game, voter: GameId<'players'>, playerIds: GameId<'players'>[]) => {
|
34 |
+
// If the voter has already voted, remove their vote
|
35 |
+
let new_votes = game.world.llmVotes.filter((vote) => vote.voter !== voter);
|
36 |
+
new_votes.push(new Votes({
|
37 |
+
voter,
|
38 |
+
playerIds,
|
39 |
+
}));
|
40 |
+
game.world.llmVotes = new_votes
|
41 |
+
}
|
42 |
+
|
43 |
+
export const gameVote = (game: Game, voter: GameId<'players'>, playerIds: GameId<'players'>[]) => {
|
44 |
+
// If the voter has already voted, remove their vote
|
45 |
+
let new_votes = game.world.gameVotes.filter((vote) => vote.voter !== voter);
|
46 |
+
new_votes.push(new Votes({
|
47 |
+
voter,
|
48 |
+
playerIds,
|
49 |
+
}));
|
50 |
+
game.world.gameVotes = new_votes
|
51 |
+
}
|
52 |
+
|
53 |
+
export const processVotes = (votes: Votes[], players: Player[], k: number = 1) => {
|
54 |
// Select the players with the most votes
|
55 |
const voteCounts: Record<GameId<'players'>, number> = {};
|
56 |
players.forEach(player => {
|
|
|
58 |
});
|
59 |
|
60 |
// Tally the votes
|
61 |
+
votes.forEach(vote => {
|
62 |
+
vote.playerIds.forEach(playerId => {
|
63 |
+
voteCounts[playerId] = (voteCounts[playerId] || 0) + 1;
|
64 |
+
});
|
65 |
});
|
66 |
|
67 |
+
// This can mean that warevolves can each other but whatever
|
68 |
const sortedVoteCounts = Object.entries(voteCounts).sort((a, b) => b[1] - a[1]);
|
69 |
+
const topKPlayers = sortedVoteCounts.slice(0, k).map((val) => {
|
70 |
+
return {
|
71 |
+
playerId: val[0] as GameId<'players'>,
|
72 |
+
voteCount: val[1],
|
73 |
+
};
|
74 |
+
});
|
75 |
+
return topKPlayers;
|
76 |
+
}
|
77 |
+
export function parseLLMVotingResult(voter: Player, log: any | null, game: Game) {
|
78 |
+
let votedPlayerId = ''
|
79 |
+
if (log?.arguments?.playerId) {
|
80 |
+
console.log('Successfully voted to eliminate villager: ', log.arguments.playerId);
|
81 |
+
votedPlayerId = log.arguments.playerId as string
|
82 |
+
} else {
|
83 |
+
const players = game.playerDescriptions.values();
|
84 |
+
const playerIds = [...players].map(player => player.playerId);
|
85 |
+
votedPlayerId = playerIds[Math.floor(Math.random() * playerIds.length)];
|
86 |
+
console.log('Voted to eliminate villager: ', votedPlayerId);
|
87 |
}
|
88 |
+
gameVote(game, voter.id, [votedPlayerId as GameId<'players'>]);
|
89 |
+
}
|
90 |
+
export async function LLmvotingCallWerewolf(werewolf: Player, villagers: PlayerDescription[]) {
|
91 |
+
// TODO: Use messages
|
92 |
+
// TODO: till fixed
|
93 |
+
return null
|
94 |
+
const inference = new HfInference();
|
95 |
+
const params = {
|
96 |
+
model: "tgi",
|
97 |
+
messages: [
|
98 |
+
{
|
99 |
+
role: "system",
|
100 |
+
content: "You are a werewolf and shall eliminate someone based on a conversation. You shall eliminate villagers. Don't make assumptions about what values to plug into functions. You MUST call a tool",
|
101 |
+
},
|
102 |
+
{ role: "user", content: `Who do you want to eliminate between the following player ? \n players : ${villagers} \n ` },
|
103 |
+
],
|
104 |
+
max_tokens: 500,
|
105 |
+
tool_choice: "auto",
|
106 |
+
tools: [
|
107 |
+
{
|
108 |
+
type: "function",
|
109 |
+
function: {
|
110 |
+
name: "vote_player",
|
111 |
+
description: "A function to chose on who to kick out",
|
112 |
+
parameters: {
|
113 |
+
type: "object",
|
114 |
+
properties: {
|
115 |
+
playerId: { type: "string", description: "The character playerId of the player to eliminate. ( eg : p:1, p:2, p:3 etc ...)" },
|
116 |
+
},
|
117 |
+
required: ["playerId"],
|
118 |
+
},
|
119 |
+
},
|
120 |
+
},
|
121 |
+
],
|
122 |
+
};
|
123 |
+
// Streaming chat completion API
|
124 |
+
const llama3 = inference.endpoint(
|
125 |
+
"https://lr2bjyq40uegzvb5.us-east-1.aws.endpoints.huggingface.cloud"
|
126 |
+
);
|
127 |
+
|
128 |
+
const response = await llama3.chatCompletion(params);
|
129 |
+
return response?.choices[0]?.message?.tool_calls?.[0]?.function || null;
|
130 |
+
}
|
131 |
+
export async function LLmvotingCallAll(villagers: PlayerDescription[]) {
|
132 |
+
// TODO: till fixed
|
133 |
+
return null
|
134 |
+
const inference = new HfInference();
|
135 |
+
const params = {
|
136 |
+
model: "tgi",
|
137 |
+
messages: [
|
138 |
+
{
|
139 |
+
role: "system",
|
140 |
+
content: "You are a villager and shall try to eliminate someone based on a conversation. You shall eliminate werewolves. Don't make assumptions about what values to plug into functions. You MUST call a tool",
|
141 |
+
},
|
142 |
+
{ role: "user", content: `Who do you want to eliminate between the following player ? \n players : ${villagers} \n ` },
|
143 |
+
],
|
144 |
+
max_tokens: 500,
|
145 |
+
tool_choice: "auto",
|
146 |
+
tools: [
|
147 |
+
{
|
148 |
+
type: "function",
|
149 |
+
function: {
|
150 |
+
name: "vote_player",
|
151 |
+
description: "A function to chose on who to kick out",
|
152 |
+
parameters: {
|
153 |
+
type: "object",
|
154 |
+
properties: {
|
155 |
+
playerId: { type: "string", description: "The character playerId of the player to eliminate. ( eg : p:1, p:2, p:3 etc ...)" },
|
156 |
+
},
|
157 |
+
required: ["playerId"],
|
158 |
+
},
|
159 |
+
},
|
160 |
+
},
|
161 |
+
],
|
162 |
+
};
|
163 |
+
// Streaming chat completion API
|
164 |
+
const llama3 = inference.endpoint(
|
165 |
+
"https://lr2bjyq40uegzvb5.us-east-1.aws.endpoints.huggingface.cloud"
|
166 |
+
);
|
167 |
+
|
168 |
+
const response = await llama3.chatCompletion(params);
|
169 |
+
return response?.choices[0]?.message?.tool_calls?.[0]?.function || null;
|
170 |
+
|
171 |
+
}
|
172 |
|
patches/convex/aiTown/world.ts
CHANGED
@@ -18,10 +18,13 @@ export const serializedWorld = {
|
|
18 |
nextId: v.number(),
|
19 |
conversations: v.array(v.object(serializedConversation)),
|
20 |
players: v.array(v.object(serializedPlayer)),
|
|
|
21 |
agents: v.array(v.object(serializedAgent)),
|
22 |
historicalLocations: v.optional(historicalLocations),
|
23 |
gameCycle: v.object(gameCycleSchema),
|
24 |
-
|
|
|
|
|
25 |
};
|
26 |
export type SerializedWorld = ObjectType<typeof serializedWorld>;
|
27 |
|
@@ -29,10 +32,13 @@ export class World {
|
|
29 |
nextId: number;
|
30 |
conversations: Map<GameId<'conversations'>, Conversation>;
|
31 |
players: Map<GameId<'players'>, Player>;
|
|
|
32 |
agents: Map<GameId<'agents'>, Agent>;
|
33 |
historicalLocations?: Map<GameId<'players'>, ArrayBuffer>;
|
34 |
gameCycle: GameCycle;
|
35 |
-
|
|
|
|
|
36 |
|
37 |
constructor(serialized: SerializedWorld) {
|
38 |
const { nextId, historicalLocations } = serialized;
|
@@ -40,10 +46,13 @@ export class World {
|
|
40 |
this.nextId = nextId;
|
41 |
this.conversations = parseMap(serialized.conversations, Conversation, (c) => c.id);
|
42 |
this.players = parseMap(serialized.players, Player, (p) => p.id);
|
|
|
43 |
this.agents = parseMap(serialized.agents, Agent, (a) => a.id);
|
44 |
this.gameCycle = new GameCycle(serialized.gameCycle);
|
45 |
-
this.
|
46 |
-
|
|
|
|
|
47 |
if (historicalLocations) {
|
48 |
this.historicalLocations = new Map();
|
49 |
for (const { playerId, location } of historicalLocations) {
|
@@ -61,6 +70,7 @@ export class World {
|
|
61 |
nextId: this.nextId,
|
62 |
conversations: [...this.conversations.values()].map((c) => c.serialize()),
|
63 |
players: [...this.players.values()].map((p) => p.serialize()),
|
|
|
64 |
agents: [...this.agents.values()].map((a) => a.serialize()),
|
65 |
historicalLocations:
|
66 |
this.historicalLocations &&
|
@@ -69,7 +79,9 @@ export class World {
|
|
69 |
location,
|
70 |
})),
|
71 |
gameCycle: this.gameCycle.serialize(),
|
72 |
-
|
|
|
|
|
73 |
};
|
74 |
}
|
75 |
}
|
|
|
18 |
nextId: v.number(),
|
19 |
conversations: v.array(v.object(serializedConversation)),
|
20 |
players: v.array(v.object(serializedPlayer)),
|
21 |
+
playersInit: v.array(v.object(serializedPlayer)),
|
22 |
agents: v.array(v.object(serializedAgent)),
|
23 |
historicalLocations: v.optional(historicalLocations),
|
24 |
gameCycle: v.object(gameCycleSchema),
|
25 |
+
gameVotes: v.array(v.object(VotesSchema)),
|
26 |
+
llmVotes: v.array(v.object(VotesSchema)),
|
27 |
+
winner: v.optional(v.union(v.literal('werewolves'), v.literal('villagers')))
|
28 |
};
|
29 |
export type SerializedWorld = ObjectType<typeof serializedWorld>;
|
30 |
|
|
|
32 |
nextId: number;
|
33 |
conversations: Map<GameId<'conversations'>, Conversation>;
|
34 |
players: Map<GameId<'players'>, Player>;
|
35 |
+
playersInit: Map<GameId<'players'>, Player>; // kept for voting purpose
|
36 |
agents: Map<GameId<'agents'>, Agent>;
|
37 |
historicalLocations?: Map<GameId<'players'>, ArrayBuffer>;
|
38 |
gameCycle: GameCycle;
|
39 |
+
gameVotes: Votes[];
|
40 |
+
llmVotes: Votes[];
|
41 |
+
winner?: 'werewolves' | 'villagers' | undefined
|
42 |
|
43 |
constructor(serialized: SerializedWorld) {
|
44 |
const { nextId, historicalLocations } = serialized;
|
|
|
46 |
this.nextId = nextId;
|
47 |
this.conversations = parseMap(serialized.conversations, Conversation, (c) => c.id);
|
48 |
this.players = parseMap(serialized.players, Player, (p) => p.id);
|
49 |
+
this.playersInit = parseMap(serialized.playersInit, Player, (p) => p.id);
|
50 |
this.agents = parseMap(serialized.agents, Agent, (a) => a.id);
|
51 |
this.gameCycle = new GameCycle(serialized.gameCycle);
|
52 |
+
this.gameVotes = serialized.gameVotes.map((v) => new Votes(v));
|
53 |
+
this.llmVotes = serialized.llmVotes.map((v) => new Votes(v));
|
54 |
+
this.winner = serialized.winner;
|
55 |
+
|
56 |
if (historicalLocations) {
|
57 |
this.historicalLocations = new Map();
|
58 |
for (const { playerId, location } of historicalLocations) {
|
|
|
70 |
nextId: this.nextId,
|
71 |
conversations: [...this.conversations.values()].map((c) => c.serialize()),
|
72 |
players: [...this.players.values()].map((p) => p.serialize()),
|
73 |
+
playersInit: [...this.playersInit.values()].map((p) => p.serialize()),
|
74 |
agents: [...this.agents.values()].map((a) => a.serialize()),
|
75 |
historicalLocations:
|
76 |
this.historicalLocations &&
|
|
|
79 |
location,
|
80 |
})),
|
81 |
gameCycle: this.gameCycle.serialize(),
|
82 |
+
gameVotes: this.gameVotes.map((v) => v.serialize()),
|
83 |
+
llmVotes: this.llmVotes.map((v) => v.serialize()),
|
84 |
+
winner: this.winner,
|
85 |
};
|
86 |
}
|
87 |
}
|
patches/convex/constants.ts
CHANGED
@@ -71,18 +71,20 @@ export const ACTIVITIES = [
|
|
71 |
];
|
72 |
|
73 |
export const ENGINE_ACTION_DURATION = 30000;
|
74 |
-
export const DAY_DURATION = 60000;
|
75 |
-
export const NIGHT_DURATION = 60000;
|
76 |
-
export const WWOLF_VOTE_DURATION = 30000;
|
77 |
-
export const PLAYER_KILL_VOTE_DURATION = 30000;
|
78 |
-
export const LLM_VOTE_DURATION = 60000;
|
79 |
|
80 |
// Debugging
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
|
|
|
|
86 |
|
87 |
// Bound the number of pathfinding searches we do per game step.
|
88 |
export const MAX_PATHFINDS_PER_STEP = 16;
|
|
|
71 |
];
|
72 |
|
73 |
export const ENGINE_ACTION_DURATION = 30000;
|
74 |
+
// export const DAY_DURATION = 60000;
|
75 |
+
// export const NIGHT_DURATION = 60000;
|
76 |
+
// export const WWOLF_VOTE_DURATION = 30000;
|
77 |
+
// export const PLAYER_KILL_VOTE_DURATION = 30000;
|
78 |
+
// export const LLM_VOTE_DURATION = 60000;
|
79 |
|
80 |
// Debugging
|
81 |
+
export const DAY_DURATION = 5000;
|
82 |
+
export const NIGHT_DURATION = 5000;
|
83 |
+
export const WWOLF_VOTE_DURATION = 1000;
|
84 |
+
export const PLAYER_KILL_VOTE_DURATION = 1000;
|
85 |
+
export const LLM_VOTE_DURATION = 1000;
|
86 |
+
|
87 |
+
export const MAX_NPC = 8;
|
88 |
|
89 |
// Bound the number of pathfinding searches we do per game step.
|
90 |
export const MAX_PATHFINDS_PER_STEP = 16;
|
patches/convex/init.ts
CHANGED
@@ -6,7 +6,7 @@ import * as map from '../data/gentle';
|
|
6 |
import { insertInput } from './aiTown/insertInput';
|
7 |
import { Id } from './_generated/dataModel';
|
8 |
import { createEngine } from './aiTown/main';
|
9 |
-
import { ENGINE_ACTION_DURATION } from './constants';
|
10 |
import { assertApiKey } from './util/llm';
|
11 |
|
12 |
const init = mutation({
|
@@ -28,7 +28,7 @@ const init = mutation({
|
|
28 |
worldStatus.engineId,
|
29 |
);
|
30 |
if (shouldCreate) {
|
31 |
-
const toCreate = args.numAgents !== undefined ? args.numAgents : Descriptions.length;
|
32 |
for (let i = 0; i < toCreate; i++) {
|
33 |
await insertInput(ctx, worldStatus.worldId, 'createAgent', {
|
34 |
descriptionIndex: i % Descriptions.length,
|
@@ -59,16 +59,15 @@ async function getOrCreateDefaultWorld(ctx: MutationCtx) {
|
|
59 |
agents: [],
|
60 |
conversations: [],
|
61 |
players: [],
|
|
|
62 |
// initialize game cycle counter
|
63 |
gameCycle: {
|
64 |
currentTime: 0,
|
65 |
cycleState: 'Day',
|
66 |
cycleIndex: 0,
|
67 |
},
|
68 |
-
|
69 |
-
|
70 |
-
votes: [],
|
71 |
-
},
|
72 |
});
|
73 |
const worldStatusId = await ctx.db.insert('worldStatus', {
|
74 |
engineId: engineId,
|
|
|
6 |
import { insertInput } from './aiTown/insertInput';
|
7 |
import { Id } from './_generated/dataModel';
|
8 |
import { createEngine } from './aiTown/main';
|
9 |
+
import { ENGINE_ACTION_DURATION, MAX_NPC } from './constants';
|
10 |
import { assertApiKey } from './util/llm';
|
11 |
|
12 |
const init = mutation({
|
|
|
28 |
worldStatus.engineId,
|
29 |
);
|
30 |
if (shouldCreate) {
|
31 |
+
const toCreate = args.numAgents !== undefined ? args.numAgents : MAX_NPC; //Descriptions.length;
|
32 |
for (let i = 0; i < toCreate; i++) {
|
33 |
await insertInput(ctx, worldStatus.worldId, 'createAgent', {
|
34 |
descriptionIndex: i % Descriptions.length,
|
|
|
59 |
agents: [],
|
60 |
conversations: [],
|
61 |
players: [],
|
62 |
+
playersInit: [],
|
63 |
// initialize game cycle counter
|
64 |
gameCycle: {
|
65 |
currentTime: 0,
|
66 |
cycleState: 'Day',
|
67 |
cycleIndex: 0,
|
68 |
},
|
69 |
+
gameVotes: [],
|
70 |
+
llmVotes: []
|
|
|
|
|
71 |
});
|
72 |
const worldStatusId = await ctx.db.insert('worldStatus', {
|
73 |
engineId: engineId,
|
patches/convex/system/schema.ts
ADDED
File without changes
|
patches/convex/world.ts
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
import { ConvexError, v } from 'convex/values';
|
2 |
import { internalMutation, mutation, query } from './_generated/server';
|
3 |
import { characters } from '../data/characters';
|
4 |
-
import { Descriptions } from '../data/characters';
|
5 |
import { insertInput } from './aiTown/insertInput';
|
|
|
6 |
import {
|
7 |
DEFAULT_NAME,
|
8 |
ENGINE_ACTION_DURATION,
|
@@ -99,6 +99,7 @@ export const userStatus = query({
|
|
99 |
args: {
|
100 |
worldId: v.id('worlds'),
|
101 |
oauthToken: v.optional(v.string()),
|
|
|
102 |
},
|
103 |
handler: async (ctx, args) => {
|
104 |
const { worldId, oauthToken } = args;
|
@@ -111,10 +112,12 @@ export const userStatus = query({
|
|
111 |
},
|
112 |
});
|
113 |
|
|
|
114 |
export const joinWorld = mutation({
|
115 |
args: {
|
116 |
worldId: v.id('worlds'),
|
117 |
oauthToken: v.optional(v.string()),
|
|
|
118 |
},
|
119 |
handler: async (ctx, args) => {
|
120 |
const { worldId, oauthToken } = args;
|
@@ -127,23 +130,37 @@ export const joinWorld = mutation({
|
|
127 |
// }
|
128 |
// const name =
|
129 |
// identity.givenName || identity.nickname || (identity.email && identity.email.split('@')[0]);
|
130 |
-
const name = oauthToken;
|
131 |
-
|
132 |
-
// if (!name) {
|
133 |
-
// throw new ConvexError(`Missing name on ${JSON.stringify(identity)}`);
|
134 |
// }
|
135 |
const world = await ctx.db.get(args.worldId);
|
136 |
if (!world) {
|
137 |
throw new ConvexError(`Invalid world ID: ${args.worldId}`);
|
138 |
}
|
139 |
-
|
140 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
// const { tokenIdentifier } = identity;
|
142 |
return await insertInput(ctx, world._id, 'join', {
|
143 |
name: randomCharacter.name,
|
144 |
character: randomCharacter.character,
|
145 |
description: randomCharacter.identity,
|
146 |
-
|
|
|
147 |
// By default everybody is a villager
|
148 |
type: 'villager',
|
149 |
});
|
@@ -178,6 +195,7 @@ export const leaveWorld = mutation({
|
|
178 |
});
|
179 |
},
|
180 |
});
|
|
|
181 |
export const sendWorldInput = mutation({
|
182 |
args: {
|
183 |
engineId: v.id('engines'),
|
|
|
1 |
import { ConvexError, v } from 'convex/values';
|
2 |
import { internalMutation, mutation, query } from './_generated/server';
|
3 |
import { characters } from '../data/characters';
|
|
|
4 |
import { insertInput } from './aiTown/insertInput';
|
5 |
+
import { Descriptions } from '../data/characters';
|
6 |
import {
|
7 |
DEFAULT_NAME,
|
8 |
ENGINE_ACTION_DURATION,
|
|
|
99 |
args: {
|
100 |
worldId: v.id('worlds'),
|
101 |
oauthToken: v.optional(v.string()),
|
102 |
+
|
103 |
},
|
104 |
handler: async (ctx, args) => {
|
105 |
const { worldId, oauthToken } = args;
|
|
|
112 |
},
|
113 |
});
|
114 |
|
115 |
+
|
116 |
export const joinWorld = mutation({
|
117 |
args: {
|
118 |
worldId: v.id('worlds'),
|
119 |
oauthToken: v.optional(v.string()),
|
120 |
+
|
121 |
},
|
122 |
handler: async (ctx, args) => {
|
123 |
const { worldId, oauthToken } = args;
|
|
|
130 |
// }
|
131 |
// const name =
|
132 |
// identity.givenName || identity.nickname || (identity.email && identity.email.split('@')[0]);
|
|
|
|
|
|
|
|
|
133 |
// }
|
134 |
const world = await ctx.db.get(args.worldId);
|
135 |
if (!world) {
|
136 |
throw new ConvexError(`Invalid world ID: ${args.worldId}`);
|
137 |
}
|
138 |
+
|
139 |
+
const playerIds = [...world.playersInit.values()].map(player => player.id)
|
140 |
+
|
141 |
+
const playerDescriptions = await ctx.db
|
142 |
+
.query('playerDescriptions')
|
143 |
+
.withIndex('worldId', (q) => q.eq('worldId', args.worldId))
|
144 |
+
.collect();
|
145 |
+
|
146 |
+
const namesInGame = playerDescriptions.map(description => {
|
147 |
+
if (playerIds.includes(description.playerId)) {
|
148 |
+
return description.name
|
149 |
+
}
|
150 |
+
})
|
151 |
+
const availableDescriptions = Descriptions.filter(
|
152 |
+
description => !namesInGame.includes(description.name)
|
153 |
+
);
|
154 |
+
|
155 |
+
const randomCharacter = availableDescriptions[Math.floor(Math.random() * availableDescriptions.length)];
|
156 |
+
|
157 |
// const { tokenIdentifier } = identity;
|
158 |
return await insertInput(ctx, world._id, 'join', {
|
159 |
name: randomCharacter.name,
|
160 |
character: randomCharacter.character,
|
161 |
description: randomCharacter.identity,
|
162 |
+
// description: `${identity.givenName} is a human player`,
|
163 |
+
tokenIdentifier: oauthToken, // TODO: change for multiplayer to oauth
|
164 |
// By default everybody is a villager
|
165 |
type: 'villager',
|
166 |
});
|
|
|
195 |
});
|
196 |
},
|
197 |
});
|
198 |
+
|
199 |
export const sendWorldInput = mutation({
|
200 |
args: {
|
201 |
engineId: v.id('engines'),
|
patches/src/App.tsx
CHANGED
@@ -13,7 +13,7 @@ import ReactModal from 'react-modal';
|
|
13 |
import MusicButton from './components/buttons/MusicButton.tsx';
|
14 |
import Button from './components/buttons/Button.tsx';
|
15 |
import InteractButton from './components/buttons/InteractButton.tsx';
|
16 |
-
import OAuthLogin from './components
|
17 |
import FreezeButton from './components/FreezeButton.tsx';
|
18 |
import { MAX_HUMAN_PLAYERS } from '../convex/constants.ts';
|
19 |
import PoweredByConvex from './components/PoweredByConvex.tsx';
|
@@ -62,21 +62,33 @@ export default function Home() {
|
|
62 |
</p>
|
63 |
</div>
|
64 |
</ReactModal>
|
|
|
|
|
|
|
|
|
65 |
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
<Game />
|
68 |
|
69 |
<footer className="justify-end bottom-0 left-0 w-full flex items-center mt-4 gap-3 p-6 flex-wrap pointer-events-none">
|
70 |
<div className="flex gap-4 flex-grow pointer-events-none">
|
71 |
-
|
72 |
-
Help
|
73 |
-
</Button>
|
74 |
<MusicButton />
|
|
|
|
|
|
|
75 |
<InteractButton />
|
76 |
-
<
|
77 |
-
|
|
|
|
|
78 |
</div>
|
79 |
-
|
80 |
</footer>
|
81 |
<ToastContainer position="bottom-right" autoClose={2000} closeOnClick theme="dark" />
|
82 |
</div>
|
@@ -84,25 +96,24 @@ export default function Home() {
|
|
84 |
);
|
85 |
}
|
86 |
|
87 |
-
|
88 |
const modalStyles = {
|
89 |
overlay: {
|
90 |
backgroundColor: 'rgb(0, 0, 0, 75%)',
|
91 |
-
zIndex:
|
92 |
},
|
93 |
content: {
|
94 |
-
top: '
|
95 |
-
left: '
|
96 |
right: 'auto',
|
97 |
bottom: 'auto',
|
98 |
marginRight: '-50%',
|
99 |
transform: 'translate(-50%, -50%)',
|
100 |
-
maxWidth: '
|
101 |
|
102 |
-
border: '
|
103 |
borderRadius: '0',
|
104 |
background: 'rgb(35, 38, 58)',
|
105 |
color: 'white',
|
106 |
fontFamily: '"Upheaval Pro", "sans-serif"',
|
107 |
},
|
108 |
-
};
|
|
|
13 |
import MusicButton from './components/buttons/MusicButton.tsx';
|
14 |
import Button from './components/buttons/Button.tsx';
|
15 |
import InteractButton from './components/buttons/InteractButton.tsx';
|
16 |
+
import OAuthLogin from './components/buttons/OAuthLogin.tsx';
|
17 |
import FreezeButton from './components/FreezeButton.tsx';
|
18 |
import { MAX_HUMAN_PLAYERS } from '../convex/constants.ts';
|
19 |
import PoweredByConvex from './components/PoweredByConvex.tsx';
|
|
|
62 |
</p>
|
63 |
</div>
|
64 |
</ReactModal>
|
65 |
+
{/*<div className="p-3 absolute top-0 right-0 z-10 text-2xl">
|
66 |
+
<Authenticated>
|
67 |
+
<UserButton afterSignOutUrl="/ai-town" />
|
68 |
+
</Authenticated>
|
69 |
|
70 |
+
<Unauthenticated>
|
71 |
+
<LoginButton />
|
72 |
+
</Unauthenticated>
|
73 |
+
</div> */}
|
74 |
+
|
75 |
+
<div className="w-full lg:h-screen min-h-screen relative isolate overflow-hidden shadow-2xl flex flex-col justify-start">
|
76 |
+
<OAuthLogin />
|
77 |
<Game />
|
78 |
|
79 |
<footer className="justify-end bottom-0 left-0 w-full flex items-center mt-4 gap-3 p-6 flex-wrap pointer-events-none">
|
80 |
<div className="flex gap-4 flex-grow pointer-events-none">
|
81 |
+
<FreezeButton />
|
|
|
|
|
82 |
<MusicButton />
|
83 |
+
<Button href="https://github.com/a16z-infra/ai-town" imgUrl={starImg}>
|
84 |
+
Star
|
85 |
+
</Button>
|
86 |
<InteractButton />
|
87 |
+
<Button imgUrl={helpImg} onClick={() => setHelpModalOpen(true)}>
|
88 |
+
Help
|
89 |
+
</Button>
|
90 |
+
<div id="footer-buttons"/>
|
91 |
</div>
|
|
|
92 |
</footer>
|
93 |
<ToastContainer position="bottom-right" autoClose={2000} closeOnClick theme="dark" />
|
94 |
</div>
|
|
|
96 |
);
|
97 |
}
|
98 |
|
|
|
99 |
const modalStyles = {
|
100 |
overlay: {
|
101 |
backgroundColor: 'rgb(0, 0, 0, 75%)',
|
102 |
+
zIndex: 12,
|
103 |
},
|
104 |
content: {
|
105 |
+
top: '50%',
|
106 |
+
left: '50%',
|
107 |
right: 'auto',
|
108 |
bottom: 'auto',
|
109 |
marginRight: '-50%',
|
110 |
transform: 'translate(-50%, -50%)',
|
111 |
+
maxWidth: '50%',
|
112 |
|
113 |
+
border: '10px solid rgb(23, 20, 33)',
|
114 |
borderRadius: '0',
|
115 |
background: 'rgb(35, 38, 58)',
|
116 |
color: 'white',
|
117 |
fontFamily: '"Upheaval Pro", "sans-serif"',
|
118 |
},
|
119 |
+
};
|
patches/src/components/Cloud.tsx
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Sprite, Stage } from "@pixi/react";
|
2 |
+
import { useQuery } from "convex/react";
|
3 |
+
import { api } from "../../convex/_generated/api";
|
4 |
+
import { Id } from "../../convex/_generated/dataModel";
|
5 |
+
|
6 |
+
export const Cloud = ({
|
7 |
+
worldId
|
8 |
+
}: {
|
9 |
+
worldId: Id<"worlds">
|
10 |
+
}) => {
|
11 |
+
return (
|
12 |
+
<img src="/ai-town/assets/cloud.jpg" className="absolute w-full h-full object-cover" />
|
13 |
+
)
|
14 |
+
}
|
15 |
+
|
patches/src/components/EndGame.tsx
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ServerGame } from "@/hooks/serverGame"
|
2 |
+
import { GameId } from "../../convex/aiTown/ids";
|
3 |
+
|
4 |
+
export function EndGame({
|
5 |
+
game,
|
6 |
+
playerId
|
7 |
+
}: {
|
8 |
+
game: ServerGame
|
9 |
+
playerId: GameId<'players'>
|
10 |
+
}) {
|
11 |
+
const llms = [...game.world.playersInit.values()].filter(player => !player.human);
|
12 |
+
const playerVotes = [...game.world.llmVotes].filter((vote) => vote.voter === playerId);
|
13 |
+
if (playerVotes.length === 0) {
|
14 |
+
return <p>You didn't vote</p>
|
15 |
+
}
|
16 |
+
const correctVotes = playerVotes[0].playerIds.filter((playerId) => llms.map(llm => llm.id).includes(playerId));
|
17 |
+
return (
|
18 |
+
<>
|
19 |
+
<h2>LLM Voting results</h2>
|
20 |
+
<p>You managed to guess {correctVotes.length} out of {llms.length}</p>
|
21 |
+
<p>The LLM were: {llms.map(llm => game.playerDescriptions.get(llm.id)?.name).join(', ')}</p>
|
22 |
+
</>
|
23 |
+
)
|
24 |
+
}
|
25 |
+
|
patches/src/components/Game.tsx
CHANGED
@@ -10,45 +10,65 @@ import { useWorldHeartbeat } from '../hooks/useWorldHeartbeat.ts';
|
|
10 |
import { useHistoricalTime } from '../hooks/useHistoricalTime.ts';
|
11 |
import { DebugTimeManager } from './DebugTimeManager.tsx';
|
12 |
import { GameId } from '../../convex/aiTown/ids.ts';
|
13 |
-
import {
|
14 |
-
import {
|
15 |
import { GameCycle } from '../../convex/aiTown/gameCycle.ts';
|
16 |
import { PlayerDescription } from '../../convex/aiTown/playerDescription.ts';
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
export const SHOW_DEBUG_UI = !!import.meta.env.VITE_SHOW_DEBUG_UI;
|
19 |
|
20 |
-
export function
|
21 |
-
switch (
|
22 |
-
case '
|
23 |
return {
|
24 |
-
|
25 |
-
desc: '
|
26 |
-
type: 'warewolf-vote',
|
27 |
};
|
28 |
-
case '
|
29 |
return {
|
30 |
-
|
31 |
-
desc: 'Select a player
|
32 |
-
type: 'player-kill',
|
33 |
};
|
34 |
-
|
35 |
return {
|
36 |
-
|
37 |
-
desc: 'Select a player to
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
};
|
40 |
}
|
41 |
}
|
42 |
|
43 |
-
export function
|
44 |
-
return gameCycle.
|
45 |
}
|
46 |
|
47 |
-
function
|
48 |
-
|
49 |
-
return gameCycle.cycleIndex === 2 || gameCycle.cycleIndex == 1;
|
50 |
}
|
51 |
|
|
|
|
|
|
|
|
|
52 |
|
53 |
export default function Game() {
|
54 |
const convex = useConvex();
|
@@ -57,7 +77,6 @@ export default function Game() {
|
|
57 |
id: GameId<'players'>;
|
58 |
}>();
|
59 |
const [gameWrapperRef, { width, height }] = useElementSize();
|
60 |
-
|
61 |
const worldStatus = useQuery(api.world.defaultWorldStatus);
|
62 |
const worldId = worldStatus?.worldId;
|
63 |
const engineId = worldStatus?.engineId;
|
@@ -71,19 +90,22 @@ export default function Game() {
|
|
71 |
const { historicalTime, timeManager } = useHistoricalTime(worldState?.engine);
|
72 |
|
73 |
const scrollViewRef = useRef<HTMLDivElement>(null);
|
74 |
-
// TODO: base this on the game state
|
75 |
-
const [gameState, setGameState] = useState<'warewolf-vote' | 'player-kill' | 'none'>('none');
|
76 |
|
77 |
-
|
|
|
78 |
return null;
|
79 |
}
|
|
|
|
|
|
|
|
|
80 |
return (
|
81 |
<>
|
82 |
{SHOW_DEBUG_UI && <DebugTimeManager timeManager={timeManager} width={200} height={100} />}
|
83 |
<div className="mx-auto w-full max-w grid grid-rows-[240px_1fr] lg:grid-rows-[1fr] lg:grid-cols-[1fr_auto] lg:grow max-w-[1400px] min-h-[480px] game-frame">
|
84 |
{/* Game area */}
|
85 |
<div className="relative overflow-hidden bg-brown-900" ref={gameWrapperRef}>
|
86 |
-
<div className={`absolute inset-0 ${
|
87 |
<div className="container">
|
88 |
<Stage width={width} height={height} options={{ backgroundColor: 0x7ab5ff }}>
|
89 |
{/* Re-propagate context because contexts are not shared between renderers.
|
@@ -102,25 +124,37 @@ https://github.com/michalochman/react-pixi-fiber/issues/145#issuecomment-5315492
|
|
102 |
</Stage>
|
103 |
</div>
|
104 |
</div>
|
105 |
-
<div
|
|
|
|
|
106 |
</div>
|
107 |
{/* Right column area */}
|
108 |
<div
|
109 |
className="flex flex-col overflow-y-auto shrink-0 px-4 py-6 sm:px-6 lg:w-96 xl:pr-6 border-t-8 sm:border-t-0 sm:border-l-8 border-brown-900 bg-brown-800 text-brown-100"
|
110 |
ref={scrollViewRef}
|
111 |
>
|
112 |
-
|
113 |
-
|
|
|
|
|
|
|
|
|
114 |
worldId={worldId}
|
115 |
engineId={engineId}
|
116 |
game={game}
|
117 |
playerId={selectedElement?.id}
|
118 |
setSelectedElement={setSelectedElement}
|
119 |
scrollViewRef={scrollViewRef}
|
120 |
-
/>
|
121 |
-
}
|
122 |
</div>
|
123 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
</>
|
125 |
);
|
126 |
}
|
|
|
10 |
import { useHistoricalTime } from '../hooks/useHistoricalTime.ts';
|
11 |
import { DebugTimeManager } from './DebugTimeManager.tsx';
|
12 |
import { GameId } from '../../convex/aiTown/ids.ts';
|
13 |
+
import { Game as GameObj} from '../../convex/aiTown/game.ts';
|
14 |
+
import { ServerGame, useServerGame } from '../hooks/serverGame.ts';
|
15 |
import { GameCycle } from '../../convex/aiTown/gameCycle.ts';
|
16 |
import { PlayerDescription } from '../../convex/aiTown/playerDescription.ts';
|
17 |
+
import { Cloud } from './Cloud.tsx';
|
18 |
+
import VotingPopover from './LLMVote.tsx';
|
19 |
+
import { createPortal } from 'react-dom';
|
20 |
+
import GameVote from './GameVote.tsx';
|
21 |
+
import { EndGame } from './EndGame.tsx';
|
22 |
|
23 |
export const SHOW_DEBUG_UI = !!import.meta.env.VITE_SHOW_DEBUG_UI;
|
24 |
|
25 |
+
export function GameStateLabel(game: GameObj, me: PlayerDescription | undefined) {
|
26 |
+
switch (game.world.gameCycle.cycleState) {
|
27 |
+
case 'Day':
|
28 |
return {
|
29 |
+
label: 'Day',
|
30 |
+
desc: 'Find out who is a werewolf',
|
|
|
31 |
};
|
32 |
+
case 'WerewolfVoting':
|
33 |
return {
|
34 |
+
label: 'Werewolf Vote',
|
35 |
+
desc: 'Select a player who is a warewolf',
|
|
|
36 |
};
|
37 |
+
case 'PlayerKillVoting':
|
38 |
return {
|
39 |
+
label: 'Player Kill Vote',
|
40 |
+
desc: me?.type === 'werewolf' ? 'Select a player to kill' : 'Hide in your home!!',
|
41 |
+
};
|
42 |
+
case 'LobbyState':
|
43 |
+
return {
|
44 |
+
label: 'Lobby (waiting for start)',
|
45 |
+
desc: 'Waiting for the game to start',
|
46 |
+
};
|
47 |
+
case 'Night':
|
48 |
+
return {
|
49 |
+
label: 'Night',
|
50 |
+
desc: me?.type === 'werewolf' ? 'Discuss who to kill with other warewolves' : 'Hide in your home!!',
|
51 |
+
};
|
52 |
+
case 'EndGame':
|
53 |
+
return {
|
54 |
+
label: 'The End',
|
55 |
+
desc: `Winners are ${ game.world.winner }!`,
|
56 |
};
|
57 |
}
|
58 |
}
|
59 |
|
60 |
+
export function canVote(game: ServerGame, me: PlayerDescription | undefined) {
|
61 |
+
return me && (game.world.gameCycle.cycleState === "WerewolfVoting" || (game.world.gameCycle.cycleState === "PlayerKillVoting" && me.type === "werewolf"));
|
62 |
}
|
63 |
|
64 |
+
export function isEndGame(game: ServerGame) {
|
65 |
+
return game.world.gameCycle.cycleState === "EndGame";
|
|
|
66 |
}
|
67 |
|
68 |
+
function showMap(gameCycle: GameCycle, me: PlayerDescription | undefined) {
|
69 |
+
// Here also check for player description
|
70 |
+
return (gameCycle.cycleState === "Day" || gameCycle.cycleState === "WerewolfVoting") || me?.type === "werewolf";
|
71 |
+
}
|
72 |
|
73 |
export default function Game() {
|
74 |
const convex = useConvex();
|
|
|
77 |
id: GameId<'players'>;
|
78 |
}>();
|
79 |
const [gameWrapperRef, { width, height }] = useElementSize();
|
|
|
80 |
const worldStatus = useQuery(api.world.defaultWorldStatus);
|
81 |
const worldId = worldStatus?.worldId;
|
82 |
const engineId = worldStatus?.engineId;
|
|
|
90 |
const { historicalTime, timeManager } = useHistoricalTime(worldState?.engine);
|
91 |
|
92 |
const scrollViewRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
93 |
|
94 |
+
const humanTokenIdentifier = useQuery(api.world.userStatus, worldId ? { worldId } : 'skip');
|
95 |
+
if (!worldId || !engineId || !game || !humanTokenIdentifier) {
|
96 |
return null;
|
97 |
}
|
98 |
+
const playerId = [...game.world.players.values()].find(
|
99 |
+
(p) => p.human === humanTokenIdentifier,
|
100 |
+
)?.id;
|
101 |
+
const meDescription = playerId ? game?.playerDescriptions.get(playerId) : undefined;
|
102 |
return (
|
103 |
<>
|
104 |
{SHOW_DEBUG_UI && <DebugTimeManager timeManager={timeManager} width={200} height={100} />}
|
105 |
<div className="mx-auto w-full max-w grid grid-rows-[240px_1fr] lg:grid-rows-[1fr] lg:grid-cols-[1fr_auto] lg:grow max-w-[1400px] min-h-[480px] game-frame">
|
106 |
{/* Game area */}
|
107 |
<div className="relative overflow-hidden bg-brown-900" ref={gameWrapperRef}>
|
108 |
+
<div className={`absolute inset-0 ${showMap(game.world.gameCycle, meDescription) ? '' : 'invisible' }`}>
|
109 |
<div className="container">
|
110 |
<Stage width={width} height={height} options={{ backgroundColor: 0x7ab5ff }}>
|
111 |
{/* Re-propagate context because contexts are not shared between renderers.
|
|
|
124 |
</Stage>
|
125 |
</div>
|
126 |
</div>
|
127 |
+
<div className={`absolute inset-0 ${!showMap(game.world.gameCycle, meDescription) ? '' : 'invisible' }`}>
|
128 |
+
<Cloud worldId={worldId} />
|
129 |
+
</div>
|
130 |
</div>
|
131 |
{/* Right column area */}
|
132 |
<div
|
133 |
className="flex flex-col overflow-y-auto shrink-0 px-4 py-6 sm:px-6 lg:w-96 xl:pr-6 border-t-8 sm:border-t-0 sm:border-l-8 border-brown-900 bg-brown-800 text-brown-100"
|
134 |
ref={scrollViewRef}
|
135 |
>
|
136 |
+
<div className="flex flex-col items-center mb-4 gap-4">
|
137 |
+
<h2 className="text-2xl font-bold">{GameStateLabel(game as GameObj, meDescription).label}</h2>
|
138 |
+
<p className="text-lg text-center">{GameStateLabel(game as GameObj, meDescription).desc}</p>
|
139 |
+
</div>
|
140 |
+
{playerId && !isEndGame(game) && canVote(game, meDescription) && <GameVote engineId={engineId} game={game} playerId={playerId} />}
|
141 |
+
{!isEndGame(game) && !canVote(game, meDescription) && <PlayerDetails
|
142 |
worldId={worldId}
|
143 |
engineId={engineId}
|
144 |
game={game}
|
145 |
playerId={selectedElement?.id}
|
146 |
setSelectedElement={setSelectedElement}
|
147 |
scrollViewRef={scrollViewRef}
|
148 |
+
/>}
|
149 |
+
{playerId && isEndGame(game) && <EndGame game={game} playerId={playerId} />}
|
150 |
</div>
|
151 |
</div>
|
152 |
+
{createPortal(
|
153 |
+
<div className="max-w-[1400px] mx-auto">
|
154 |
+
{playerId && <VotingPopover engineId={engineId} game={game} playerId={playerId} />}
|
155 |
+
</div>,
|
156 |
+
document.getElementById('footer-buttons')
|
157 |
+
)}
|
158 |
</>
|
159 |
);
|
160 |
}
|
patches/src/components/GameVote.tsx
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ServerGame } from "@/hooks/serverGame";
|
2 |
+
import { Id } from "../../convex/_generated/dataModel";
|
3 |
+
import { GameId } from "../../convex/aiTown/ids";
|
4 |
+
import { canVote } from "./Game";
|
5 |
+
import { useSendInput } from "../hooks/sendInput";
|
6 |
+
import { VoteModal } from "./VoteModal";
|
7 |
+
import { useState } from "react";
|
8 |
+
|
9 |
+
const getSelectablePlayers = (game: ServerGame, playerId: GameId<'players'>, players: Player[]) => {
|
10 |
+
if (game.world.gameCycle.cycleState === "WerewolfVoting") {
|
11 |
+
return players.filter(
|
12 |
+
(player) => (player.id !== playerId)
|
13 |
+
);
|
14 |
+
}
|
15 |
+
|
16 |
+
else if (game.world.gameCycle.cycleState === "PlayerKillVoting") {
|
17 |
+
return players.filter(
|
18 |
+
(player) => (player.id !== playerId) && game.playerDescriptions.get(player.id)?.type === 'villager'
|
19 |
+
);
|
20 |
+
}
|
21 |
+
return []
|
22 |
+
}
|
23 |
+
|
24 |
+
|
25 |
+
export default function GameVote({
|
26 |
+
engineId,
|
27 |
+
game,
|
28 |
+
playerId,
|
29 |
+
}: {
|
30 |
+
engineId: Id<'engines'>,
|
31 |
+
game: ServerGame,
|
32 |
+
playerId: GameId<'players'>,
|
33 |
+
}) {
|
34 |
+
const inputVote = useSendInput(engineId, "gameVote");
|
35 |
+
const [votes, setVotes] = useState<GameId<'players'>[]>([]);
|
36 |
+
const players = getSelectablePlayers(game, playerId, [...game.world.players.values()])
|
37 |
+
return (
|
38 |
+
<VoteModal
|
39 |
+
compact={true}
|
40 |
+
game={game}
|
41 |
+
engineId={engineId}
|
42 |
+
playerId={playerId}
|
43 |
+
maxVotes={1}
|
44 |
+
votes={votes}
|
45 |
+
players={players}
|
46 |
+
onVote={(newVotes) => {
|
47 |
+
setVotes(newVotes);
|
48 |
+
inputVote({voter: playerId, votedPlayerIds: newVotes});
|
49 |
+
}}
|
50 |
+
/>
|
51 |
+
);
|
52 |
+
}
|
53 |
+
|
patches/src/components/LLMVote.tsx
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* v0 by Vercel.
|
3 |
+
* @see https://v0.dev/t/OH0xMc8eRYc
|
4 |
+
* Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app
|
5 |
+
*/
|
6 |
+
|
7 |
+
import Button from "./buttons/Button"
|
8 |
+
import Modal from 'react-modal';
|
9 |
+
import { GameId } from "../../convex/aiTown/ids"
|
10 |
+
import { Id } from "../../convex/_generated/dataModel"
|
11 |
+
import { ServerGame } from "@/hooks/serverGame"
|
12 |
+
import { useState } from "react"
|
13 |
+
import { VoteModal } from "./VoteModal";
|
14 |
+
import { useSendInput } from "../hooks/sendInput";
|
15 |
+
|
16 |
+
export default function LLMVote(
|
17 |
+
{
|
18 |
+
game,
|
19 |
+
engineId,
|
20 |
+
playerId,
|
21 |
+
}: {
|
22 |
+
engineId: Id<'engines'>,
|
23 |
+
game: ServerGame,
|
24 |
+
playerId: GameId<'players'>,
|
25 |
+
}
|
26 |
+
) {
|
27 |
+
const [isModalOpen, setIsModalOpen] = useState(false)
|
28 |
+
const [votes, setVotes] = useState<GameId<'players'>[]>([]);
|
29 |
+
const players = [...game.world.playersInit.values()].filter(
|
30 |
+
(player) => player.id !== playerId
|
31 |
+
)
|
32 |
+
const inputVote = useSendInput(engineId, "llmVote");
|
33 |
+
const totalLLMs = players.filter((player) => !player.human).length
|
34 |
+
return (
|
35 |
+
<>
|
36 |
+
<Button onClick={() => setIsModalOpen(true)} className="lg:block">
|
37 |
+
Choose LLM
|
38 |
+
</Button>
|
39 |
+
<Modal
|
40 |
+
isOpen={isModalOpen}
|
41 |
+
onRequestClose={() => setIsModalOpen(false)}
|
42 |
+
contentLabel="Choose LLM"
|
43 |
+
ariaHideApp={false}
|
44 |
+
className="mx-auto bg-brown-800 p-16 game-frame font-body max-w-[1000px]"
|
45 |
+
style={{
|
46 |
+
content: {
|
47 |
+
position: 'absolute',
|
48 |
+
top: '50%',
|
49 |
+
left: '30%',
|
50 |
+
transform: 'translate(-20%, -50%)',
|
51 |
+
},
|
52 |
+
}}
|
53 |
+
>
|
54 |
+
<h2 className="text-2xl font-bold text-center mb-8">Which players are LLMs ? (Choose up to {totalLLMs} players)</h2>
|
55 |
+
<div className="max-w-[600px] w-full mx-auto">
|
56 |
+
<VoteModal compact={false} votes={votes} players={players} onVote={(newVotes) => {
|
57 |
+
setVotes(newVotes)
|
58 |
+
inputVote({voter: playerId, votedPlayerIds: newVotes});
|
59 |
+
}} engineId={engineId} game={game} playerId={playerId} maxVotes={totalLLMs} />
|
60 |
+
</div>
|
61 |
+
</Modal>
|
62 |
+
</>
|
63 |
+
)
|
64 |
+
}
|
patches/src/components/PixiGame.tsx
CHANGED
@@ -93,16 +93,16 @@ export const PixiGame = (props: {
|
|
93 |
// Effect hook to change the tileSet based on a day/night condition
|
94 |
useEffect(() => {
|
95 |
const { cycleState } = props.game.world.gameCycle;
|
96 |
-
const tileSet = (cycleState === '
|
97 |
? {
|
98 |
-
background: props.game.worldMap.bgTiles,
|
99 |
-
objectMap: props.game.worldMap.objectTiles,
|
100 |
-
decor: props.game.worldMap.decorTiles,
|
101 |
-
}
|
102 |
-
: {
|
103 |
background: props.game.worldMap.bgTilesN,
|
104 |
objectMap: props.game.worldMap.objectTilesN,
|
105 |
decor: props.game.worldMap.decorTilesN,
|
|
|
|
|
|
|
|
|
|
|
106 |
};
|
107 |
setCurrentTileSet(tileSet);
|
108 |
}, [props.game.world.gameCycle.cycleState]);
|
|
|
93 |
// Effect hook to change the tileSet based on a day/night condition
|
94 |
useEffect(() => {
|
95 |
const { cycleState } = props.game.world.gameCycle;
|
96 |
+
const tileSet = (cycleState === 'Night' || cycleState === 'PlayerKillVoting')
|
97 |
? {
|
|
|
|
|
|
|
|
|
|
|
98 |
background: props.game.worldMap.bgTilesN,
|
99 |
objectMap: props.game.worldMap.objectTilesN,
|
100 |
decor: props.game.worldMap.decorTilesN,
|
101 |
+
}
|
102 |
+
: {
|
103 |
+
background: props.game.worldMap.bgTiles,
|
104 |
+
objectMap: props.game.worldMap.objectTiles,
|
105 |
+
decor: props.game.worldMap.decorTiles,
|
106 |
};
|
107 |
setCurrentTileSet(tileSet);
|
108 |
}, [props.game.world.gameCycle.cycleState]);
|
patches/src/components/PixiStaticMap.tsx
CHANGED
@@ -158,7 +158,8 @@ const PixiStaticMap = React.memo(
|
|
158 |
(prevProps, nextProps) => {
|
159 |
return (
|
160 |
prevProps.map.bgTiles === nextProps.map.bgTiles &&
|
161 |
-
prevProps.map.objectTiles === nextProps.map.objectTiles
|
|
|
162 |
);
|
163 |
}
|
164 |
);
|
|
|
158 |
(prevProps, nextProps) => {
|
159 |
return (
|
160 |
prevProps.map.bgTiles === nextProps.map.bgTiles &&
|
161 |
+
prevProps.map.objectTiles === nextProps.map.objectTiles &&
|
162 |
+
prevProps.map.decorTiles === nextProps.map.decorTiles
|
163 |
);
|
164 |
}
|
165 |
);
|
patches/src/components/PlayerDetails.tsx
CHANGED
@@ -73,11 +73,10 @@ export default function PlayerDetails({
|
|
73 |
humanConversation &&
|
74 |
playerConversation &&
|
75 |
humanConversation.id === playerConversation.id;
|
76 |
-
|
77 |
const humanStatus =
|
78 |
humanPlayer && humanConversation && humanConversation.participants.get(humanPlayer.id)?.status;
|
79 |
const playerStatus = playerConversation && playerConversation.participants.get(playerId)?.status;
|
80 |
-
|
81 |
const haveInvite = sameConversation && humanStatus?.kind === 'invited';
|
82 |
const waitingForAccept =
|
83 |
sameConversation && playerConversation.participants.get(playerId)?.status.kind === 'invited';
|
@@ -226,7 +225,7 @@ export default function PlayerDetails({
|
|
226 |
<div className="desc my-6">
|
227 |
<p className="leading-tight -m-4 bg-brown-700 text-base sm:text-sm">
|
228 |
{!isMe && playerDescription?.description}
|
229 |
-
{isMe && <i>This is you
|
230 |
{!isMe && inConversationWithMe && (
|
231 |
<>
|
232 |
<br />
|
|
|
73 |
humanConversation &&
|
74 |
playerConversation &&
|
75 |
humanConversation.id === playerConversation.id;
|
76 |
+
|
77 |
const humanStatus =
|
78 |
humanPlayer && humanConversation && humanConversation.participants.get(humanPlayer.id)?.status;
|
79 |
const playerStatus = playerConversation && playerConversation.participants.get(playerId)?.status;
|
|
|
80 |
const haveInvite = sameConversation && humanStatus?.kind === 'invited';
|
81 |
const waitingForAccept =
|
82 |
sameConversation && playerConversation.participants.get(playerId)?.status.kind === 'invited';
|
|
|
225 |
<div className="desc my-6">
|
226 |
<p className="leading-tight -m-4 bg-brown-700 text-base sm:text-sm">
|
227 |
{!isMe && playerDescription?.description}
|
228 |
+
{isMe && <i>This is you! You are a {playerDescription?.type}</i>}
|
229 |
{!isMe && inConversationWithMe && (
|
230 |
<>
|
231 |
<br />
|
patches/src/components/StartGameButton.tsx
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useMutation, useQuery } from 'convex/react';
|
2 |
+
import { api } from '../../convex/_generated/api';
|
3 |
+
import Button from './buttons/Button';
|
4 |
+
|
5 |
+
export default function StartGameButton() {
|
6 |
+
const stopAllowed = useQuery(api.testing.stopAllowed) ?? false;
|
7 |
+
const defaultWorld = useQuery(api.world.defaultWorldStatus);
|
8 |
+
|
9 |
+
const frozen = defaultWorld?.status === 'stoppedByDeveloper';
|
10 |
+
|
11 |
+
const unfreeze = useMutation(api.testing.resume);
|
12 |
+
const freeze = useMutation(api.testing.stop);
|
13 |
+
|
14 |
+
const flipSwitch = async () => {
|
15 |
+
if (frozen) {
|
16 |
+
console.log('Unfreezing');
|
17 |
+
await unfreeze();
|
18 |
+
} else {
|
19 |
+
console.log('Freezing');
|
20 |
+
await freeze();
|
21 |
+
}
|
22 |
+
};
|
23 |
+
|
24 |
+
return !stopAllowed ? null : (
|
25 |
+
<>
|
26 |
+
<Button
|
27 |
+
onClick={flipSwitch}
|
28 |
+
className="hidden lg:block"
|
29 |
+
title="When freezing a world, the agents will take some time to stop what they are doing before they become frozen. "
|
30 |
+
imgUrl="/assets/star.svg"
|
31 |
+
>
|
32 |
+
{frozen ? 'Unfreeze' : 'Freeze'}
|
33 |
+
</Button>
|
34 |
+
</>
|
35 |
+
);
|
36 |
+
}
|
patches/src/components/VoteModal.tsx
CHANGED
@@ -1,110 +1,72 @@
|
|
1 |
import { ServerGame } from "@/hooks/serverGame";
|
2 |
-
import { SelectElement } from "./Player";
|
3 |
import { GameId } from "../../convex/aiTown/ids";
|
4 |
import Button from "./buttons/Button";
|
5 |
-
import { useQuery } from "convex/react";
|
6 |
-
import { api } from "../../convex/_generated/api";
|
7 |
import { Id } from '../../convex/_generated/dataModel';
|
8 |
import { characters } from "../../data/characters";
|
9 |
-
import {
|
10 |
-
import { BaseTexture, SCALE_MODES, Spritesheet } from "pixi.js";
|
11 |
-
import { Sprite, Stage } from "@pixi/react";
|
12 |
import { Character } from "./Character";
|
13 |
-
import {
|
14 |
-
|
15 |
-
export type Vote = (id: GameId<'players'>) => void;
|
16 |
-
|
17 |
-
export function VotingName(gameCycle: GameCycle) {
|
18 |
-
switch (gameCycle.cycleIndex) {
|
19 |
-
case 2:
|
20 |
-
return {
|
21 |
-
name: 'Warewolf Vote',
|
22 |
-
desc: 'Select a player who is warewolf',
|
23 |
-
type: 'WarewolfVote',
|
24 |
-
};
|
25 |
-
case 3:
|
26 |
-
return {
|
27 |
-
name: 'Player Kill',
|
28 |
-
desc: 'Select a player to kill',
|
29 |
-
type: 'PlayerKill',
|
30 |
-
};
|
31 |
-
default:
|
32 |
-
return {
|
33 |
-
name: 'Error',
|
34 |
-
desc: 'Select a player to vote',
|
35 |
-
type: 'Error'
|
36 |
-
};
|
37 |
-
}
|
38 |
-
}
|
39 |
-
|
40 |
export const VoteModal = ({
|
41 |
-
worldId,
|
42 |
engineId,
|
43 |
game,
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
}: {
|
45 |
-
worldId: Id<'worlds'>,
|
46 |
engineId: Id<'engines'>,
|
47 |
game: ServerGame,
|
48 |
-
|
|
|
|
|
|
|
|
|
|
|
49 |
}) => {
|
50 |
-
const
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
}
|
57 |
-
const gameState = "warewolf-vote"
|
58 |
-
|
59 |
-
|
60 |
-
const humanTokenIdentifier = useQuery(api.world.userStatus, {worldId});
|
61 |
-
const [spriteSheet, setSpriteSheet] = useState<Spritesheet>();
|
62 |
-
const character = characters[0]
|
63 |
-
useEffect(() => {
|
64 |
-
const parseSheet = async () => {
|
65 |
-
const sheet = new Spritesheet(
|
66 |
-
BaseTexture.from(character.textureUrl, {
|
67 |
-
scaleMode: SCALE_MODES.NEAREST,
|
68 |
-
}),
|
69 |
-
character.spritesheetData,
|
70 |
-
);
|
71 |
-
await sheet.parse();
|
72 |
-
setSpriteSheet(sheet);
|
73 |
-
};
|
74 |
-
void parseSheet();
|
75 |
-
}, []);
|
76 |
-
// TODO only let people select non-dead players
|
77 |
-
const selectablePlayers = [...game.world.players.values()].filter(
|
78 |
-
(player) => player.id !== humanTokenIdentifier
|
79 |
-
);
|
80 |
return (
|
81 |
-
|
82 |
-
|
83 |
-
<h2 className="text-2xl font-bold">{VotingName(game.world.gameCycle).name}</h2>
|
84 |
-
<p className="text-lg">{VotingName(game.world.gameCycle).desc}</p>
|
85 |
-
</div>
|
86 |
-
{selectablePlayers.map((playable) => {
|
87 |
const playerDesc = game.playerDescriptions.get(playable.id);
|
88 |
const character = characters.find((c) => c.name === playerDesc?.character);
|
89 |
if (!character) return null;
|
|
|
90 |
return (
|
91 |
<>
|
92 |
-
<Button onClick={() =>
|
93 |
-
className="lg:block border-2 border-gold"
|
94 |
-
title={
|
|
|
95 |
key={playable.id}
|
|
|
96 |
>
|
97 |
-
{
|
98 |
<Stage width={30} height={40} options={{backgroundAlpha: 0.0}}>
|
99 |
{
|
100 |
-
|
101 |
}
|
102 |
</Stage>
|
103 |
</Button>
|
104 |
</>
|
105 |
)
|
106 |
})}
|
107 |
-
|
108 |
)
|
109 |
|
110 |
|
|
|
1 |
import { ServerGame } from "@/hooks/serverGame";
|
|
|
2 |
import { GameId } from "../../convex/aiTown/ids";
|
3 |
import Button from "./buttons/Button";
|
|
|
|
|
4 |
import { Id } from '../../convex/_generated/dataModel';
|
5 |
import { characters } from "../../data/characters";
|
6 |
+
import { Stage } from "@pixi/react";
|
|
|
|
|
7 |
import { Character } from "./Character";
|
8 |
+
import { Player } from "../../convex/aiTown/player";
|
9 |
+
export type Vote = (id: GameId<'players'>[]) => void;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
export const VoteModal = ({
|
|
|
11 |
engineId,
|
12 |
game,
|
13 |
+
playerId,
|
14 |
+
maxVotes=1,
|
15 |
+
players,
|
16 |
+
votes,
|
17 |
+
onVote,
|
18 |
+
compact
|
19 |
}: {
|
|
|
20 |
engineId: Id<'engines'>,
|
21 |
game: ServerGame,
|
22 |
+
playerId: GameId<'players'>,
|
23 |
+
maxVotes: number,
|
24 |
+
players: Player[],
|
25 |
+
votes: GameId<'players'>[],
|
26 |
+
onVote: (votes: GameId<'players'>[]) => void,
|
27 |
+
compact: boolean
|
28 |
}) => {
|
29 |
+
const vote = (playerId: GameId<'players'>) => {
|
30 |
+
let newVotes = votes;
|
31 |
+
if (votes.includes(playerId)) {
|
32 |
+
newVotes = newVotes.filter((vote) => vote !== playerId);
|
33 |
+
} else {
|
34 |
+
newVotes = [...votes, playerId];
|
35 |
+
}
|
36 |
+
if (newVotes.length > maxVotes) {
|
37 |
+
// Removed the first vote and add the new vote
|
38 |
+
newVotes = newVotes.slice(1);
|
39 |
+
}
|
40 |
+
console.log(`votes: ${newVotes.map((vote) => game.playerDescriptions.get(vote)?.name).join(", ")}`)
|
41 |
+
onVote(newVotes);
|
42 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
return (
|
44 |
+
<div className={`flex gap-4 ${compact ? "flex-col" : "flex-wrap justify-center"}`}>
|
45 |
+
{players.map((playable) => {
|
|
|
|
|
|
|
|
|
46 |
const playerDesc = game.playerDescriptions.get(playable.id);
|
47 |
const character = characters.find((c) => c.name === playerDesc?.character);
|
48 |
if (!character) return null;
|
49 |
+
const selected = votes.includes(playable.id);
|
50 |
return (
|
51 |
<>
|
52 |
+
<Button onClick={() => vote(playable.id)}
|
53 |
+
className="lg:block border-2 border-gold min-w-[100px]"
|
54 |
+
title={playerDesc?.name}
|
55 |
+
selected={selected}
|
56 |
key={playable.id}
|
57 |
+
disabled={selected}
|
58 |
>
|
59 |
+
{playerDesc?.name}
|
60 |
<Stage width={30} height={40} options={{backgroundAlpha: 0.0}}>
|
61 |
{
|
62 |
+
<Character textureUrl={character.textureUrl} isViewer={true} spritesheetData={character.spritesheetData} x={15} y={15} orientation={0} onClick={() => {} } />
|
63 |
}
|
64 |
</Stage>
|
65 |
</Button>
|
66 |
</>
|
67 |
)
|
68 |
})}
|
69 |
+
</div>
|
70 |
)
|
71 |
|
72 |
|
patches/src/components/buttons/Button.tsx
CHANGED
@@ -8,12 +8,16 @@ export default function Button(props: {
|
|
8 |
onClick?: MouseEventHandler;
|
9 |
title?: string;
|
10 |
children: ReactNode;
|
|
|
|
|
11 |
}) {
|
12 |
return (
|
13 |
<a
|
14 |
className={clsx(
|
15 |
'button text-white shadow-solid text-xl pointer-events-auto',
|
16 |
props.className,
|
|
|
|
|
17 |
)}
|
18 |
href={props.href}
|
19 |
title={props.title}
|
|
|
8 |
onClick?: MouseEventHandler;
|
9 |
title?: string;
|
10 |
children: ReactNode;
|
11 |
+
selected?: boolean;
|
12 |
+
disabled?: boolean;
|
13 |
}) {
|
14 |
return (
|
15 |
<a
|
16 |
className={clsx(
|
17 |
'button text-white shadow-solid text-xl pointer-events-auto',
|
18 |
props.className,
|
19 |
+
props.selected && 'button-selected',
|
20 |
+
props.disabled && 'disabled',
|
21 |
)}
|
22 |
href={props.href}
|
23 |
title={props.title}
|
patches/src/components/buttons/InteractButton.tsx
CHANGED
@@ -17,7 +17,6 @@ export default function InteractButton() {
|
|
17 |
const game = useServerGame(worldId);
|
18 |
const oauth = JSON.parse(localStorage.getItem('oauth'));
|
19 |
const oauthToken = oauth ? oauth.userInfo.fullname : undefined;
|
20 |
-
console.log(oauthToken)
|
21 |
const humanTokenIdentifier = useQuery(api.world.userStatus, worldId ? { worldId, oauthToken } : 'skip');
|
22 |
const userPlayerId =
|
23 |
game && [...game.world.players.values()].find((p) => p.human === humanTokenIdentifier)?.id;
|
@@ -47,7 +46,6 @@ export default function InteractButton() {
|
|
47 |
[convex, join, oauthToken],
|
48 |
);
|
49 |
|
50 |
-
|
51 |
const joinOrLeaveGame = () => {
|
52 |
if (
|
53 |
!worldId ||
|
|
|
17 |
const game = useServerGame(worldId);
|
18 |
const oauth = JSON.parse(localStorage.getItem('oauth'));
|
19 |
const oauthToken = oauth ? oauth.userInfo.fullname : undefined;
|
|
|
20 |
const humanTokenIdentifier = useQuery(api.world.userStatus, worldId ? { worldId, oauthToken } : 'skip');
|
21 |
const userPlayerId =
|
22 |
game && [...game.world.players.values()].find((p) => p.human === humanTokenIdentifier)?.id;
|
|
|
46 |
[convex, join, oauthToken],
|
47 |
);
|
48 |
|
|
|
49 |
const joinOrLeaveGame = () => {
|
50 |
if (
|
51 |
!worldId ||
|
patches/src/index.css
CHANGED
@@ -40,8 +40,7 @@
|
|
40 |
|
41 |
body {
|
42 |
color: rgb(var(--foreground-rgb));
|
43 |
-
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb)))
|
44 |
-
rgb(var(--background-start-rgb));
|
45 |
}
|
46 |
|
47 |
.game-background {
|
@@ -148,6 +147,10 @@ body {
|
|
148 |
transform: translateY(-15%);
|
149 |
}
|
150 |
|
|
|
|
|
|
|
|
|
151 |
@media (max-width: 640px) {
|
152 |
.button {
|
153 |
height: 40px;
|
@@ -155,8 +158,8 @@ body {
|
|
155 |
font-size: 16px;
|
156 |
}
|
157 |
|
158 |
-
.button
|
159 |
-
.button
|
160 |
vertical-align: top;
|
161 |
line-height: 1;
|
162 |
}
|
@@ -182,4 +185,4 @@ p[contenteditable='true']:empty::before {
|
|
182 |
|
183 |
.shape-top-left-corner {
|
184 |
clip-path: polygon(0 0, 100% 0, 0 100%);
|
185 |
-
}
|
|
|
40 |
|
41 |
body {
|
42 |
color: rgb(var(--foreground-rgb));
|
43 |
+
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
|
|
|
44 |
}
|
45 |
|
46 |
.game-background {
|
|
|
147 |
transform: translateY(-15%);
|
148 |
}
|
149 |
|
150 |
+
.button-selected {
|
151 |
+
border-image-source: url(../assets/ui/button_pressed.svg);
|
152 |
+
}
|
153 |
+
|
154 |
@media (max-width: 640px) {
|
155 |
.button {
|
156 |
height: 40px;
|
|
|
158 |
font-size: 16px;
|
159 |
}
|
160 |
|
161 |
+
.button>div,
|
162 |
+
.button>span {
|
163 |
vertical-align: top;
|
164 |
line-height: 1;
|
165 |
}
|
|
|
185 |
|
186 |
.shape-top-left-corner {
|
187 |
clip-path: polygon(0 0, 100% 0, 0 100%);
|
188 |
+
}
|