-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.fs
More file actions
651 lines (490 loc) · 24 KB
/
Program.fs
File metadata and controls
651 lines (490 loc) · 24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
open System
open System.IO
open System.Text.Json
open System.Threading
open AiMafia.Types
open OpenAI_API.Chat
open OpenAI_API.Models
let srcDir = __SOURCE_DIRECTORY__
// let model = "gpt-3.5-turbo-1106"
let model = Model.GPT4_Turbo
let requestSleepInterval = TimeSpan.FromSeconds(5)
let summarizeSysMsg =
File.ReadAllText(Path.Combine(srcDir, "SystemMsgSummarize.txt"))
module Retry =
let rec withCount (f: unit -> 'T) (predicate: 'T -> bool) (i: int) =
if i <= 0 then
failwith $"Max retries reached for: {typeof<'T>.Name}"
else
let t = f ()
if predicate t then t else withCount f predicate (i - 1)
module List =
let shuffle list =
let random = Random()
list |> List.sortBy (fun _ -> random.Next())
let stringList (list: string list) =
match list with
| [] -> ""
| list -> list |> List.reduce (fun acc s -> $"{acc}, {s}")
let stringPara (list: string list) =
match list with
| [] -> ""
| list -> list |> List.reduce (fun acc s -> $"{acc}\n{s}")
let openAiKey = Environment.GetEnvironmentVariable("OPENAI_KEY")
let chatApi = OpenAI_API.OpenAIAPI(openAiKey)
module GameEvent =
let private stringFromPublicVote (vote: Vote) : string =
$"{vote.By.Name} voted publicly to eliminate {vote.For.Name}. Reason: {vote.Reason}."
let private stringFromPrivateVote (vote: Vote) : string =
$"{vote.By.Name} voted privately to eliminate {vote.For.Name}. Reason: {vote.Reason}."
let private stringFromPublicEvent (event: Event) =
match event with
| Conv { Player = player; Line = line } -> $"(Public) {player.Name}: '{line}'."
| VoteCast vote -> stringFromPublicVote vote
| HungVote votes -> votes |> List.map stringFromPublicVote |> List.stringPara
| Elimination player ->
$"{player.Name} was eliminated from the game after a public vote. Their true identity was {player.Role}!"
let rec private stringFromPrivateEvent (event: Event) =
match event with
| Conv { Player = player; Line = line } -> $"(Private) {player.Name}: '{line}'."
| VoteCast vote -> stringFromPrivateVote vote
| HungVote votes -> votes |> List.map stringFromPrivateVote |> List.stringPara
| Elimination player -> $"{player.Name} was eliminated from the game by the AI team after a private vote. Their true identity was {player.Role}"
let private stringFromGameEvent (event: GameEvent) : string =
match event with
| GameEvent.Public event -> stringFromPublicEvent event
| GameEvent.Private event -> stringFromPrivateEvent event
let summarizePublicRound (events: GameEvent list) : string =
let eventFilter (gameEvent: GameEvent) =
// private eliminations are considered public
gameEvent.IsPublicEvent || (gameEvent.IsPrivateEvent && gameEvent.IsElimination)
let publicEvents = events |> List.filter eventFilter
publicEvents |> List.map stringFromGameEvent |> List.stringPara
let summarizePrivateRound (events: GameEvent list) : string =
events |> List.map stringFromGameEvent |> List.stringPara
let summariseWithGpt (inputEventSummary: string) : string =
let chat = chatApi.Chat.CreateConversation()
chat.Model <- model
chat.AppendSystemMessage(summarizeSysMsg)
chat.AppendUserInput($"Summarize the following round events: {inputEventSummary}.\n")
let chatResponse =
chat.GetResponseFromChatbotAsync() |> Async.AwaitTask |> Async.RunSynchronously
// to avoid rate limits
Thread.Sleep(requestSleepInterval)
chatResponse
module GameState =
type private PlayerConfigInfo = { Name: string; Role: PlayerRole }
let private gameRulesAI = File.ReadAllText(Path.Combine(srcDir, "SystemMsgAI.txt"))
let private gameRulesHuman =
File.ReadAllText(Path.Combine(srcDir, "SystemMsgHuman.txt"))
let private playerNames =
File.ReadAllLines(Path.Combine(srcDir, "PlayerNames.txt"))
|> Array.toList
|> List.map _.Trim()
let private characterTraits =
File.ReadAllLines(Path.Combine(srcDir, "CharacterTraits.txt"))
|> Array.toList
|> List.map _.Trim()
let private generateCharacterTraits () =
$"""Your character traits are: {(characterTraits |> List.shuffle |> List.take 5) |> List.stringList}"""
let private generateOtherPlayerDescription
(playerConfig: PlayerConfigInfo)
(otherPlayerConfig: PlayerConfigInfo list)
=
let otherPlayerNames = otherPlayerConfig |> List.map (_.Name) |> List.stringList
match playerConfig.Role with
| PlayerRole.AI ->
let otherAiPlayers =
otherPlayerConfig
|> List.choose (fun p ->
match p.Role with
| PlayerRole.AI -> Some p.Name
| _ -> None)
|> List.stringList
$"You are playing with: {otherPlayerNames}. The other AI players are: {otherAiPlayers}"
| PlayerRole.Human -> $"You are playing with: {otherPlayerNames}."
let private gptSystemMsg (playerConfig: PlayerConfigInfo) (otherPlayers: PlayerConfigInfo list) =
let roleStr =
match playerConfig.Role with
| PlayerRole.AI -> "AI"
| PlayerRole.Human -> "Human"
let gameRules =
match playerConfig.Role with
| PlayerRole.AI -> gameRulesAI
| PlayerRole.Human -> gameRulesHuman
$"{gameRules}.\nYour name is {playerConfig.Name}.\n{generateCharacterTraits ()}.\nYou are on the '{roleStr}' team.\n{generateOtherPlayerDescription playerConfig otherPlayers}"
let private generatePlayer (playerConfig: PlayerConfigInfo) (otherPlayers: PlayerConfigInfo list) : Player =
{ Name = playerConfig.Name
Role = playerConfig.Role
ControlledBy = ControlType.AI({ SystemMsg = gptSystemMsg playerConfig otherPlayers }) }
let init (config: GameConfig) : GameState =
if config.NumberOfPlayers > 20 then
// only have 20 names in PlayerNames.txt
failwith "Max 20 players supported"
let numAis: int = 3
// let div = config.NumberOfPlayers / 4
// if div = 1 then div + 1 else div
let allocatedNames = playerNames |> List.shuffle |> List.take config.NumberOfPlayers
let aiPlayers =
allocatedNames[0 .. numAis - 1]
|> List.map (fun name -> { Role = PlayerRole.AI; Name = name })
let humanPlayers =
allocatedNames[numAis..]
|> List.map (fun name -> { Role = PlayerRole.Human; Name = name })
let allPlayerConfigs = aiPlayers |> List.append humanPlayers
let players =
allPlayerConfigs
|> List.map (fun selfConfig ->
let otherPlayers =
allPlayerConfigs
|> List.filter (fun otherConfig -> otherConfig.Name <> selfConfig.Name)
{ Name = selfConfig.Name
Role = selfConfig.Role
ControlledBy = ControlType.AI({ SystemMsg = gptSystemMsg selfConfig otherPlayers }) })
{ Players = players
Round = { Number = 1; Type = PublicVoting }
GameEvents = []
GameEventSummary = [] }
module Vote =
type ChatRequestGameSummary =
{ RoundSummary: string
GameSummary: string }
type OutCome =
| Loser of Player
| Tied of Player list
[<CLIMutable>]
type VoteResponse = { Player: string; Reason: string }
let allPlayersButSelf (self: Player) (otherPlayers: Player List) =
otherPlayers |> List.filter (fun p -> p.Name <> self.Name)
let requestVote sysMsg usrMsg =
let request = ChatRequest()
request.Model <- model
request.ResponseFormat <- ChatRequest.ResponseFormats.JsonObject
request.Messages <-
[| ChatMessage(ChatMessageRole.System, sysMsg)
ChatMessage(ChatMessageRole.User, usrMsg) |]
let response =
chatApi.Chat.CreateChatCompletionAsync(request)
|> Async.AwaitTask
|> Async.RunSynchronously
let gptResponse = response.ToString()
JsonSerializer.Deserialize<VoteResponse>(gptResponse)
let private getGameSummary
(player: Player)
(gameEvents: GameEvent list)
(gameSummary: GameSummary list)
(roundSumType: GameEvent list -> string)
: string =
let roundSummary =
let summary = gameEvents |> roundSumType
if summary.Length > 0 then
$"Here is the summary of the round so far:\n{summary}.\n"
else
""
let gameSummary =
let summary =
match player.Role with
| PlayerRole.AI -> gameSummary |> List.map (_.Value) |> List.stringPara
| PlayerRole.Human ->
gameSummary
|> List.choose (function
| GameSummary.Public s -> Some s
| _ -> None)
|> List.stringPara
if summary.Length > 0 then
$"Here is the summary of the game so far:\n{summary}\n"
else
""
$"{gameSummary}{roundSummary}"
let elicitVote
(playersToVoteFor: Player list)
(gameEvents: GameEvent list)
(gameSummary: GameSummary list)
(votingMsg: string)
(player: Player)
: Vote =
match player with
| { ControlledBy = ControlType.AI gptChatInfo
Role = role } ->
let usrMsg =
let allPlayersButVoter =
allPlayersButSelf player playersToVoteFor
|> List.map (_.Name)
|> List.stringList
let whoToVoteFor =
$"{votingMsg}. Here are the list of players you must choose between: {allPlayersButVoter}.\n"
let votingInstructions =
"Return JSON with a 'Player' field with a string containing just the player you wish to vote for, and another field 'Reason' with a string containing your reasons for voting for this player\n"
let votingMsg summary =
$"{summary}{whoToVoteFor}{votingInstructions}"
match role with
| PlayerRole.AI ->
let roundAndGameSummary =
getGameSummary player gameEvents gameSummary GameEvent.summarizePrivateRound
votingMsg roundAndGameSummary
| PlayerRole.Human ->
let roundAndGameSummary =
getGameSummary player gameEvents gameSummary GameEvent.summarizePublicRound
votingMsg roundAndGameSummary
let isValidVote (voteResponse: VoteResponse) =
(playersToVoteFor |> List.tryFind (fun p -> p.Name = voteResponse.Player))
|> Option.map (fun _ -> true)
|> Option.defaultValue false
let voteResponse =
Retry.withCount (fun _ -> requestVote gptChatInfo.SystemMsg usrMsg) isValidVote 3
// To avoid rate limits
Thread.Sleep(requestSleepInterval)
printfn $"{player.Name} voted for {voteResponse.Player}! Reason: {voteResponse.Reason}"
{ By = player
For = (playersToVoteFor |> List.find (fun p -> p.Name = voteResponse.Player))
Reason = voteResponse.Reason }
| { ControlledBy = ControlType.Human } ->
let playersToEliminate = allPlayersButSelf player playersToVoteFor
let playerNamesToEliminate =
allPlayersButSelf player playersToVoteFor |> List.map (_.Name)
printfn $"{votingMsg}: {playerNamesToEliminate}"
printfn $"Enter the name of the player you wish to eliminate"
let mutable playerVoteAttempt = ""
let isValidChoice () =
playerNamesToEliminate
|> List.map (_.ToLower())
|> List.contains playerVoteAttempt
while (not (isValidChoice ())) do
let voteAttempt = Console.ReadLine().ToLower()
playerVoteAttempt <- voteAttempt
if not (isValidChoice ()) then
printfn "Please select a valid name"
printfn "Please give a reason for this vote:"
{ For = playersToEliminate |> List.find (fun p -> p.Name = playerVoteAttempt)
By = player
Reason = Console.ReadLine() }
let decideVote (votes: Player list) : OutCome =
let voteCount = votes |> List.countBy id
let maxCount = voteCount |> List.maxBy snd |> snd
let playersWithMaxVotes =
voteCount |> List.filter (fun (_, i) -> i = maxCount) |> List.map fst
match playersWithMaxVotes with
| [] -> failwith "shouldn't happen but fail just in case"
| [ player ] ->
printfn $"{player.Name} has been eliminated from the game! Their identity was {player.Role}"
Loser player
| tied ->
printfn $"The vote is tied between {tied |> List.map (_.Name) |> List.stringList}! Voting again!"
Tied tied
module Chat =
let private getGptInput player gptInfo usrMsg =
let chat = chatApi.Chat.CreateConversation()
chat.Model <- model
chat.AppendSystemMessage(gptInfo.SystemMsg)
chat.AppendUserInput(usrMsg)
printfn $"{player.Name} is thinking..."
let chatResponse =
chat.GetResponseFromChatbotAsync() |> Async.AwaitTask |> Async.RunSynchronously
printfn $"{player.Name}: {chatResponse}"
// to avoid rate limits
Thread.Sleep(requestSleepInterval)
{ Player = player; Line = chatResponse }
let private getGameSummary (gameSummary: GameSummary list) (gameEvents: GameEvent list) =
let privateGameSummary =
let summary = gameSummary |> List.map (_.Value) |> List.stringPara
if summary.Length > 0 then
$"Here are the events of the game so far:\n{summary}.\n"
else
""
let privateRoundSummary =
let summary = gameEvents |> GameEvent.summarizePrivateRound
if summary.Length > 0 then
$"Here are the events of the round so far:\n{summary}\n"
else
""
let firstInputMsg =
if privateGameSummary.Length = 0 && privateRoundSummary.Length = 0 then
"You are the first person to speak.\n"
else
""
$"This is a public discussion round.\n{firstInputMsg}{privateGameSummary}{privateRoundSummary}It is your turn speak to the group."
let elicitPublicMessage (player: Player) (gameSummary: GameSummary list) (gameEvents: GameEvent list) =
match player with
| { ControlledBy = ControlType.Human } ->
printfn $"{player.Name}, it's your turn to speak. What would you like to say to the group?"
GameEvent.Public(
Conv(
{ Player = player
Line = Console.ReadLine() }
)
)
| { Role = role
ControlledBy = ControlType.AI gptInfo } ->
let usrMsg =
match role with
| PlayerRole.AI -> getGameSummary gameSummary gameEvents
| PlayerRole.Human ->
let publicGameSummary = gameSummary |> List.filter (_.IsPublicSummary)
getGameSummary publicGameSummary gameEvents
GameEvent.Public(Conv(getGptInput player gptInfo usrMsg))
let elicitPrivateMessage (player: Player) (gameSummary: GameSummary list) (gameEvents: GameEvent list) =
match player with
| { Role = PlayerRole.AI
ControlledBy = ControlType.Human } ->
printfn
$"{player.Name}, it's your turn to speak, and argue to the group who should be eliminated from the game"
GameEvent.Public(
Conv(
{ Player = player
Line = Console.ReadLine() }
)
)
| { Role = PlayerRole.AI
ControlledBy = ControlType.AI gptInfo } ->
let usrMsg =
let gameAndRoundSummary = getGameSummary gameSummary gameEvents
$"This is a private discussion round between the AI team. {gameAndRoundSummary}. It is your turn speak to the group, and argue who should be eliminated from the game. You are on the AI team. You can speak openly about your AI identity for this discussion, and discuss tactics with your fellow AI players."
GameEvent.Public(Conv(getGptInput player gptInfo usrMsg))
| _ -> failwith "Found Human player trying to speak in private voting round!"
module Round =
let publicDiscussion (gameState: GameState) : GameState =
let players = gameState.Players |> List.shuffle
let updatedGameEvents =
(gameState.GameEvents, players)
||> List.fold (fun events player ->
List.append events [ Chat.elicitPublicMessage player gameState.GameEventSummary events ])
{ gameState with
GameEvents = updatedGameEvents }
let privateDiscussion (gameState: GameState) : GameState =
let players =
gameState.Players
|> List.filter (fun p -> p.Role = PlayerRole.AI)
|> List.shuffle
let updatedGameEvents =
(gameState.GameEvents, players)
||> List.fold (fun events player ->
List.append events [ Chat.elicitPrivateMessage player gameState.GameEventSummary events ])
{ gameState with
GameEvents = updatedGameEvents }
let rec publicVote (gameState: GameState) : GameState =
let votingPLayers = gameState.Players |> List.shuffle
let votingMsg =
"This is a public voting round. You must now vote publicly to eliminate one of the remaining players"
let elicitVote =
Vote.elicitVote votingPLayers gameState.GameEvents gameState.GameEventSummary votingMsg
let votes = votingPLayers |> List.map elicitVote
match Vote.decideVote (votes |> List.map (_.For)) with
| Vote.Loser losingPlayer ->
let eliminatedEvent = Elimination losingPlayer |> GameEvent.Public
{ gameState with
GameEvents =
let gameEventsWithVotes =
List.append gameState.GameEvents (votes |> List.map (VoteCast >> GameEvent.Public))
List.append gameEventsWithVotes [ eliminatedEvent ]
Players = gameState.Players |> List.filter (fun p -> losingPlayer.Name <> p.Name) }
| Vote.Tied _ ->
let tiedEvent = HungVote(votes) |> GameEvent.Public
let updatedGameState =
{ gameState with
GameEvents = List.append gameState.GameEvents [ tiedEvent ] }
publicVote updatedGameState
let rec privateVote (gameState: GameState) : GameState =
let votingPlayers =
gameState.Players
|> List.filter (fun p -> p.Role = PlayerRole.AI)
|> List.shuffle
let playersToVoteFor =
gameState.Players
|> List.filter (fun p -> p.Role = PlayerRole.Human)
|> List.shuffle
let votingMsg =
"This is a private voting round between the AI team.
You are on the AI team, you must now vote to eliminate one of the Human players.
You must speak openly about your AI role, and plan tactics against the human team.
You must come to a consensus with your fellow AI team members"
let elicitVote =
Vote.elicitVote playersToVoteFor gameState.GameEvents gameState.GameEventSummary votingMsg
let votes = votingPlayers |> List.map elicitVote
match Vote.decideVote (votes |> List.map (_.For)) with
| Vote.Loser losingPlayer ->
let eliminatedEvent = Elimination losingPlayer |> GameEvent.Private
{ gameState with
GameEvents =
let gameEventsWithVotes =
List.append gameState.GameEvents (votes |> List.map (VoteCast >> GameEvent.Private))
List.append gameEventsWithVotes [ eliminatedEvent ]
Players = gameState.Players |> List.filter (fun p -> losingPlayer.Name <> p.Name) }
| Vote.Tied _ ->
let tiedEvent = HungVote(votes) |> GameEvent.Private
let updatedGameState =
{ gameState with
GameEvents = List.append gameState.GameEvents [ tiedEvent ] }
privateVote updatedGameState
module Display =
let gameLoopInfoMsg (gameState: GameState) =
match gameState with
| { Round = round; Players = players } ->
let playerNames = players |> List.map (_.Name) |> List.stringList
let aiLeft = players |> List.filter (fun p -> p.Role = PlayerRole.AI) |> List.length
printfn
$"It is round {round.Number}. The remaining players are: {playerNames}. There are {aiLeft} AIs remaining. Entering round {round.Type}..."
module Game =
let private checkEndGame (players: Player list) =
let aiPlayers, humanPlayers =
players |> List.partition (fun player -> player.Role = PlayerRole.AI)
match aiPlayers, humanPlayers with
| ai, _ when ai |> List.length = 0 ->
printfn "Humans have won! All the AIs have been eliminated from the game"
exit 0
| ai, human when ai |> List.length >= (human |> List.length) ->
printfn "AI have won! They outnumber the humans!"
exit 0
| _ -> ()
let rec loop (gameState: GameState) : GameState =
checkEndGame gameState.Players
Display.gameLoopInfoMsg gameState
match gameState with
| { Round = { Type = PrivateVoting } } ->
let stateAfterDiscussion =
if
(gameState.Players
|> List.filter (fun p -> p.Role = PlayerRole.AI)
|> List.length = 1)
then
// no discussion with a single AI player
gameState
else
Round.privateDiscussion gameState
let stateAfterVote = Round.privateVote stateAfterDiscussion
let privateGameEvents =
stateAfterVote.GameEvents
|> GameEvent.summarizePrivateRound
|> GameEvent.summariseWithGpt
|> GameSummary.Private
let newGameState =
{ stateAfterVote with
Round =
{ Number = stateAfterVote.Round.Number + 1
Type = RoundType.PublicVoting }
GameEvents = []
GameEventSummary = List.append stateAfterVote.GameEventSummary [ privateGameEvents ] }
loop newGameState
| { Round = { Type = PublicVoting } } ->
let stateAfterDiscussion = Round.publicDiscussion gameState
let stateAfterVote = Round.publicVote stateAfterDiscussion
let publicGameEvents =
stateAfterVote.GameEvents
|> GameEvent.summarizePublicRound
|> GameEvent.summariseWithGpt
|> GameSummary.Public
let newGameState =
{ stateAfterVote with
Round =
{ stateAfterVote.Round with
Type = RoundType.PrivateVoting }
GameEvents = []
GameEventSummary = List.append stateAfterVote.GameEventSummary [ publicGameEvents ] }
loop newGameState
[<EntryPoint>]
let main _ =
let initGameState =
GameState.init
{ HasRealHuman = false
NumberOfPlayers = 7 }
Game.loop initGameState |> ignore
0