Jofthomas HF staff commited on
Commit
df2ef4f
1 Parent(s): 2d097bf

bulk morning

Browse files
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
- name: 'Alex',
14
- character: 'f5',
15
- identity: `You are a fictional character whose name is Alex. You enjoy painting,
16
- programming and reading sci-fi books. You are currently talking to a human who
17
- is very interested to get to know you. You are kind but can be sarcastic. You
18
- dislike repetitive questions. You get SUPER excited about books.`,
19
- plan: 'You want to find love.',
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
- name: 'Kurt',
51
- character: 'f2',
52
- identity: `Kurt knows about everything, including science and
53
- computers and politics and history and biology. He loves talking about
54
- everything, always injecting fun facts about the topic of discussion.`,
55
- plan: 'You want to spread knowledge.',
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
- name: 'Kira',
75
- character: 'f8',
76
- identity: `Kira wants everyone to think she is happy. But deep down,
77
- she's incredibly depressed. She hides her sadness by talking about travel,
78
- food, and yoga. But often she can't keep her sadness in and will start crying.
79
- Often it seems like she is close to having a mental breakdown.`,
80
- plan: 'You want find a way to be happy.',
81
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  ];
83
 
 
84
  export const characters = [
85
  {
86
  name: 'f1',
87
- textureUrl: '/assets/32x32folk.png',
88
  spritesheetData: f1SpritesheetData,
89
- speed: 0.19,
90
  },
91
  {
92
  name: 'f2',
93
- textureUrl: '/assets/32x32folk.png',
94
  spritesheetData: f2SpritesheetData,
95
- speed: 0.19,
96
  },
97
  {
98
  name: 'f3',
99
- textureUrl: '/assets/32x32folk.png',
100
  spritesheetData: f3SpritesheetData,
101
- speed: 0.19,
102
  },
103
  {
104
  name: 'f4',
105
- textureUrl: '/assets/32x32folk.png',
106
  spritesheetData: f4SpritesheetData,
107
- speed: 0.19,
108
  },
109
  {
110
  name: 'f5',
111
- textureUrl: '/assets/32x32folk.png',
112
  spritesheetData: f5SpritesheetData,
113
- speed: 0.19,
114
  },
115
  {
116
  name: 'f6',
117
- textureUrl: '/assets/32x32folk.png',
118
  spritesheetData: f6SpritesheetData,
119
- speed: 0.19,
120
  },
121
  {
122
  name: 'f7',
123
- textureUrl: '/assets/32x32folk.png',
124
  spritesheetData: f7SpritesheetData,
125
- speed: 0.19,
126
  },
127
  {
128
  name: 'f8',
129
- textureUrl: '/assets/32x32folk.png',
130
  spritesheetData: f8SpritesheetData,
131
- speed: 0.19,
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 = 2;
 
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: 300,
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: 300,
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: 300,
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: 500,
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 schedule operation if any.
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
- // update game cycle counter
179
- this.world.gameCycle.tick(this, this.tickDuration);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // Save each player's location into the history buffer at the end of
198
- // each tick.
199
- for (const player of this.world.players.values()) {
200
- let historicalObject = this.historicalLocations.get(player.id);
201
- if (!historicalObject) {
202
- historicalObject = new HistoricalObject(locationFields, playerLocation(player));
203
- this.historicalLocations.set(player.id, historicalObject);
 
 
 
 
 
 
 
204
  }
205
- historicalObject.update(now, playerLocation(player));
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
- type CycleState = 'Day' | 'Night' | 'WerewolfVoting' | 'PlayerKillVoting' | 'LLMsVoting' | 'LobbyState'
 
 
 
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
- LLMsVoting: LLM_VOTE_DURATION,
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('LLMsVoting'),
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.votes, [...game.world.players.values()])[0];
50
- // TODO: Kill the player
51
- const playerToKill = game.world.players.get(mostVotedPlayer)
52
- if (playerToKill != undefined) {
53
- playerToKill.kill(game, now)
54
  }
 
55
  }
56
  if (prevState === 'WerewolfVoting') {
57
- const mostVotedPlayer = processVotes(game.world.votes, [...game.world.players.values()])[0];
58
- // TODO: Check if most voted player is werewolf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- vote: inputHandler({
 
 
 
 
 
 
 
 
 
 
 
331
  args: {
332
- votedPlayerId: v.string(),
333
- voteType: v.string(),
334
  },
335
  handler: (game, now, args) => {
336
- const votedPlayerId = parseGameId('players', args.votedPlayerId);
337
- // TODO: Implement the fucntion
338
- // game.vote(votedPlayerId);
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
- export type VoteType = 'WarewolfVote' | 'PlayerKill' | 'LLMVote'
 
6
 
7
  export const VotesSchema = {
8
- votesType: v.string(),
9
- votes: v.array(v.object({
10
- playerId: playerId,
11
- voter: playerId,
12
- }))
13
  }
14
 
15
  export type SerializedVotes = ObjectType<typeof VotesSchema>;
16
  export class Votes {
17
- votesType: string;
18
- votes: {
19
- playerId: GameId<'players'>;
20
- voter: GameId<'players'>;
21
- }[];
22
 
23
  constructor(serialized: SerializedVotes) {
24
- const { votesType, votes } = serialized;
25
-
26
- this.votesType = votesType;
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 { votesType, votes } = this;
35
  return {
36
- votesType,
37
- votes,
38
  };
39
  }
40
  }
41
 
42
- export const processVotes = (votes: Votes, players: Player[], k: number = 1) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.votes.forEach(vote => {
51
- voteCounts[vote.playerId] = (voteCounts[vote.playerId] || 0) + 1;
 
 
52
  });
53
 
 
54
  const sortedVoteCounts = Object.entries(voteCounts).sort((a, b) => b[1] - a[1]);
55
- const topKPlayers = sortedVoteCounts.slice(0, k).map(entry => entry[0]);
56
- return topKPlayers as GameId<'players'>[];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- votes: v.object(VotesSchema)
 
 
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
- votes: Votes;
 
 
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.votes = new Votes(serialized.votes);
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
- votes: this.votes.serialize(),
 
 
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
- // export const DAY_DURATION = 100;
82
- // export const NIGHT_DURATION = 100;
83
- // export const WWOLF_VOTE_DURATION = 100;
84
- // export const PLAYER_KILL_VOTE_DURATION = 100;
85
- // export const LLM_VOTE_DURATION = 100;
 
 
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
- votes: {
69
- votesType: 'KillVotes',
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
- // Select a random character description
140
- const randomCharacter = Descriptions[Math.floor(Math.random() * Descriptions.length)];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- tokenIdentifier: oauthToken,
 
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//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,21 +62,33 @@ export default function Home() {
62
  </p>
63
  </div>
64
  </ReactModal>
 
 
 
 
65
 
66
- <div className="w-full lg:h-screen min-h-screen relative isolate overflow-hidden shadow-2xl flex flex-col justify-start">
 
 
 
 
 
 
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
- <Button imgUrl={helpImg} onClick={() => setHelpModalOpen(true)}>
72
- Help
73
- </Button>
74
  <MusicButton />
 
 
 
75
  <InteractButton />
76
- <OAuthLogin />
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: 0,
92
  },
93
  content: {
94
- top: '800%',
95
- left: '80%',
96
  right: 'auto',
97
  bottom: 'auto',
98
  marginRight: '-50%',
99
  transform: 'translate(-50%, -50%)',
100
- maxWidth: '90%',
101
 
102
- border: '1px solid rgb(23, 20, 33)',
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 { useServerGame } from '../hooks/serverGame.ts';
14
- import { VoteModal } from './VoteModal.tsx';
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 VotingName(gameState: string) {
21
- switch (gameState) {
22
- case 'warewolf-vote':
23
  return {
24
- name: 'Warewolf Vote',
25
- desc: 'Select a player who is warewolf',
26
- type: 'warewolf-vote',
27
  };
28
- case 'player-kill':
29
  return {
30
- name: 'Player Kill',
31
- desc: 'Select a player to kill',
32
- type: 'player-kill',
33
  };
34
- default:
35
  return {
36
- name: 'Voting',
37
- desc: 'Select a player to vote',
38
- type: 'voting',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  };
40
  }
41
  }
42
 
43
- export function isVotingState(gameCycle: GameCycle) {
44
- return gameCycle.cycleIndex === 0 || gameCycle.cycleIndex === 2;
45
  }
46
 
47
- function showMap(gameCycle: GameCycle, player: PlayerDescription) {
48
- // Here also check for player description
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
- if (!worldId || !engineId || !game) {
 
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 ${!showMap ? 'invisible' : '' }`}>
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></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
- {isVotingState(game.world.gameCycle) ? <VoteModal game={game} worldId={worldId} engineId={engineId} /> :
113
- <PlayerDetails
 
 
 
 
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 === 'Day' || cycleState === 'Night')
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!</i>}
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 { useEffect, useState } from "react";
10
- import { BaseTexture, SCALE_MODES, Spritesheet } from "pixi.js";
11
- import { Sprite, Stage } from "@pixi/react";
12
  import { Character } from "./Character";
13
- import { useSendInput } from "../hooks/sendInput";
14
- import { GameCycle } from "../../convex/aiTown/gameCycle";
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 [hasVoted, setHasVoted] = useState<boolean>(false);
51
- const vote = useSendInput(engineId, 'vote');
52
- const onVote = (playerId: GameId<'players'>) => {
53
- if (hasVoted) return;
54
- vote({votedPlayerId: playerId, voteType: gameState});
55
- setHasVoted(true);
 
 
 
 
 
 
 
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
- <div className="flex flex-col items-center mb-4">
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={() => onVote(playable.id)}
93
- className="lg:block border-2 border-gold"
94
- title={character?.name}
 
95
  key={playable.id}
 
96
  >
97
- {character?.name}
98
  <Stage width={30} height={40} options={{backgroundAlpha: 0.0}}>
99
  {
100
- spriteSheet && <Character textureUrl={character.textureUrl} isViewer={true} spritesheetData={character.spritesheetData} x={15} y={15} orientation={0} onClick={() => {} } />
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 > div,
159
- .button > span {
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
+ }