Skip to content

Commit

Permalink
Merge pull request #2 from nukeop/integrate-gamestate
Browse files Browse the repository at this point in the history
Integrate GameState into GameStateContext
  • Loading branch information
nukeop authored Jun 27, 2024
2 parents 3f338b1 + 259d245 commit 7deefb4
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 216 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"prettier": "^3.3.2",
"ts-jest": "^29.1.5",
"ts-node": "^10.9.2",
"typescript": "^5.5.2"
"typescript": "^5.5.2",
"@testing-library/react-hooks": "^8.0.0"
},
"dependencies": {
"dotenv": "^16.4.5",
Expand All @@ -39,4 +40,4 @@
"openai": "^4.52.0",
"react": "^18.3.1"
}
}
}
21 changes: 0 additions & 21 deletions src/game/GameState.test.ts

This file was deleted.

160 changes: 0 additions & 160 deletions src/game/GameState.ts

This file was deleted.

10 changes: 0 additions & 10 deletions src/game/hooks/useGameState.ts

This file was deleted.

41 changes: 41 additions & 0 deletions src/game/providers/GameStateProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { GameStateProvider, useGameState } from './GameStateProvider';
import { Player, Team } from '../Player';
import { GameLog, ActionType } from '../GameLog';
import { GameStage } from '../GameStage';

describe('GameStateProvider', () => {
it('provides game state context with initial values', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => <GameStateProvider>{children}</GameStateProvider>;
const { result } = renderHook(() => useGameState(), { wrapper });

expect(result.current.machinePlayers).toEqual([]);
expect(result.current.humanPlayer).toEqual(new Player('', Team.Humans));
expect(result.current.log).toBeInstanceOf(GameLog);
expect(result.current.stage).toBeInstanceOf(GameStage);
});

it('allows advancing game state', async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => <GameStateProvider>{children}</GameStateProvider>;
const { result } = renderHook(() => useGameState(), { wrapper });

await act(async () => {
await result.current.advance();
});

expect(result.current.stage.actingPlayer).not.toEqual(new Player('', Team.Machines));
});

it('processes player actions', async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => <GameStateProvider>{children}</GameStateProvider>;
const { result } = renderHook(() => useGameState(), { wrapper });

await act(async () => {
await result.current.processPlayerAction();
});

expect(result.current.log.messages.length).toBeGreaterThan(0);
expect(result.current.log.messages[0].actionType).toBe(ActionType.Speech);
});
});
106 changes: 93 additions & 13 deletions src/game/providers/GameStateProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,102 @@
import React, { ReactElement, createContext, useContext } from 'react';
import { GameState } from '../GameState';
import React, { createContext, useContext, useState } from 'react';
import { Player, Team } from '../Player';
import { GameLog } from '../GameLog';
import { GameStage } from '../GameStage';
import { sample } from 'lodash';
import { names, personalities } from '../../prompts';
import { OpenAiApiService } from '../../services/OpenAiService';
import { ActionType } from '../GameLog';
import Logger from '../../logger';

let Context = createContext<GameState | null>(null);
Context.displayName = 'GameStateContext';
interface GameStateContextType {
machinePlayers: Player[];
humanPlayer: Player;
log: GameLog;
stage: GameStage;
advance: () => Promise<void>;
processPlayerAction: () => Promise<void>;
}

const GameStateContext = createContext<GameStateContextType | undefined>(undefined);

export function useGameState() {
let context = useContext(Context);
if (context === null) {
export const useGameState = () => {
const context = useContext(GameStateContext);
if (context === undefined) {
throw new Error('useGameState must be used within a GameStateProvider');
}
return context;
}
};

interface Props {
value: GameState;
interface GameStateProviderProps {
children: React.ReactNode;
}

export function GameStateProvider({ value, children }: Props): ReactElement {
return <Context.Provider value={value}>{children}</Context.Provider>;
}
export const GameStateProvider: React.FC<GameStateProviderProps> = ({ children }) => {
const [machinePlayers, setMachinePlayers] = useState<Player[]>([]);
const [humanPlayer, setHumanPlayer] = useState<Player>(new Player('', Team.Humans));
const [log, setLog] = useState<GameLog>(new GameLog());
const [stage, setStage] = useState<GameStage>(new GameStage(new Player('', Team.Machines)));

const initGameState = (numberOfPlayers: number) => {
let availableNames = names;
const machinePlayersInit = Array.from({ length: numberOfPlayers }, (_) => {
const name = sample(availableNames) ?? '';
availableNames = availableNames.filter((n) => n !== name);
return new Player(name!, Team.Machines, sample(personalities)?.name);
});

const humanName = sample(names) ?? '';
const humanPlayerInit = new Player(humanName!, Team.Humans);
const stageInit = new GameStage(machinePlayersInit[0]);

setMachinePlayers(machinePlayersInit);
setHumanPlayer(humanPlayerInit);
setStage(stageInit);
};

const advance = async () => {
if (stage.actingPlayer === humanPlayer) {
stage.nextPlayer();
} else {
try {
Logger.debug(`Processing machine turn for: ${stage.actingPlayer.name}`);
await processPlayerAction();
stage.nextPlayer();
Logger.debug(`Progressing to next player: ${stage.actingPlayer.name}`);
} catch (error) {
log.addErrorMessage('An error occurred while processing the machine turn.');
log.addErrorMessage((error as Error).message);
stage.nextPlayer();
}
}
};

const processPlayerAction = async () => {
const service = new OpenAiApiService();
const response = await service.createChatCompletion({
max_tokens: 512,
model: 'gpt-3.5-turbo',
tools: [],
parallel_tool_calls: false,
messages: [{ role: 'system', content: 'Your prompt here' }],
});

const choice = response.choices[0];
Logger.debug(JSON.stringify(choice, null, 2));
const toolCall = choice.message?.tool_calls?.[0];
const message = choice.message?.content;

if (toolCall) {
const actionType: ActionType = toolCall?.function.name as ActionType;
log.addPlayerAction(stage.actingPlayer, toolCall?.function.arguments!, actionType, toolCall.id);
} else if (message) {
log.addPlayerAction(stage.actingPlayer, message, ActionType.Speech);
}
};

return (
<GameStateContext.Provider value={{ machinePlayers, humanPlayer, log, stage, advance, processPlayerAction }}>
{children}
</GameStateContext.Provider>
);
};
11 changes: 1 addition & 10 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import dotenv from 'dotenv';
import React from 'react';
import { render } from 'ink';
import { initGameState } from './game/GameState';
import { Game } from './ui/Game';
import { GameStateProvider } from './game/providers/GameStateProvider';

(async () => {
dotenv.config();
const gameState = initGameState(3);
gameState.log.addSystemMessage(
'Welcome to the LLM Mafia! You are the human player. The goal is to blend in while the machines try to eliminate you. Good luck!',
);
gameState.log.addSystemMessage(
`The machine players are:\n ${gameState.machinePlayers.map((player) => player.name).join('\n ')}.`,
);
gameState.log.addSystemMessage(`You are ${gameState.humanPlayer.name}.`);
render(
<GameStateProvider value={gameState}>
<GameStateProvider>
<Game />
</GameStateProvider>,
);
Expand Down

0 comments on commit 7deefb4

Please sign in to comment.