├── .gitattributes ├── .github └── workflows │ ├── dotnet.yml │ └── node.js.yml ├── .gitignore ├── RobotInterrogation.sln ├── RobotInterrogation ├── ClientApp │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── robots.txt │ ├── src │ │ ├── Connectivity.ts │ │ ├── components │ │ │ ├── About.tsx │ │ │ ├── Home.tsx │ │ │ ├── Host.tsx │ │ │ ├── Interview.tsx │ │ │ ├── connectInterview.ts │ │ │ ├── interviewParts │ │ │ │ ├── Disconnected.tsx │ │ │ │ ├── GameLink.css │ │ │ │ ├── InducerDisplay.tsx │ │ │ │ ├── InducerPrompt.tsx │ │ │ │ ├── InducerResponse.tsx │ │ │ │ ├── InducerWait.tsx │ │ │ │ ├── InterviewFinished.tsx │ │ │ │ ├── InterviewerInProgress.tsx │ │ │ │ ├── InterviewerPenaltySelection.tsx │ │ │ │ ├── InterviewerPositionSelection.tsx │ │ │ │ ├── InterviewerReadyToStart.tsx │ │ │ │ ├── NotYetConnected.tsx │ │ │ │ ├── OpponentDisconnected.tsx │ │ │ │ ├── OutcomeHumanCorrect.tsx │ │ │ │ ├── OutcomeHumanIncorrect.tsx │ │ │ │ ├── OutcomeRobotCorrect.tsx │ │ │ │ ├── OutcomeRobotIncorrect.tsx │ │ │ │ ├── OutcomeViolentKilled.tsx │ │ │ │ ├── PacketSelection.tsx │ │ │ │ ├── PenaltyCalibration.tsx │ │ │ │ ├── PositionSelection.tsx │ │ │ │ ├── SpectatorBackgroundSelection.tsx │ │ │ │ ├── SpectatorInProgress.tsx │ │ │ │ ├── SpectatorInducerDisplay.tsx │ │ │ │ ├── SpectatorPenaltySelection.tsx │ │ │ │ ├── SpectatorReadyToStart.tsx │ │ │ │ ├── SuspectBackgroundSelection.tsx │ │ │ │ ├── SuspectInProgress.tsx │ │ │ │ ├── SuspectPenaltySelection.tsx │ │ │ │ ├── SuspectReadyToStart.tsx │ │ │ │ ├── WaitPacketSelection.tsx │ │ │ │ ├── WaitPenalty.tsx │ │ │ │ ├── WaitingForOpponent.tsx │ │ │ │ ├── WaitingQuestionDisplay.tsx │ │ │ │ └── elements │ │ │ │ │ ├── ActionSet.tsx │ │ │ │ │ ├── Choice.tsx │ │ │ │ │ ├── ChoiceArray.tsx │ │ │ │ │ ├── Countdown.css │ │ │ │ │ ├── Countdown.tsx │ │ │ │ │ ├── Help.tsx │ │ │ │ │ ├── Interference.css │ │ │ │ │ ├── InterferencePattern.tsx │ │ │ │ │ ├── InterferenceSolution.tsx │ │ │ │ │ ├── InterviewQuestion.tsx │ │ │ │ │ ├── P.tsx │ │ │ │ │ ├── PacketDisplay.tsx │ │ │ │ │ ├── Page.tsx │ │ │ │ │ ├── PositionHeader.tsx │ │ │ │ │ ├── SortableQuestions.tsx │ │ │ │ │ ├── SuspectRole.tsx │ │ │ │ │ └── ValueDisplay.tsx │ │ │ └── interviewReducer.ts │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── setupTests.ts │ └── tsconfig.json ├── Controllers │ └── DataController.cs ├── Hubs │ └── InterviewHub.cs ├── Models │ ├── GameConfiguration.cs │ ├── IDWordList.cs │ ├── InterferencePattern.cs │ ├── Interview.cs │ ├── InterviewOutcome.cs │ ├── InterviewStatus.cs │ ├── Packet.cs │ ├── Player.cs │ ├── PlayerPosition.cs │ ├── Question.cs │ ├── SuspectRole.cs │ └── SuspectRoleType.cs ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ └── _ViewImports.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── RobotInterrogation.csproj ├── Services │ ├── ExtensionMethods.cs │ ├── InterferenceService.cs │ └── InterviewService.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json └── Tests ├── InterferenceServiceTests.cs └── Tests.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 5.0.101 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 15.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | shell: bash 30 | working-directory: RobotInterrogation/ClientApp 31 | - run: npm run build --if-present 32 | shell: bash 33 | working-directory: RobotInterrogation/ClientApp 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /RobotInterrogation.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29728.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RobotInterrogation", "RobotInterrogation\RobotInterrogation.csproj", "{F788728F-AA62-4FB1-B25E-CF222891E74D}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{2A3FA00B-353E-439C-958F-1FB5CAE9CD02}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {F788728F-AA62-4FB1-B25E-CF222891E74D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {F788728F-AA62-4FB1-B25E-CF222891E74D}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {F788728F-AA62-4FB1-B25E-CF222891E74D}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {F788728F-AA62-4FB1-B25E-CF222891E74D}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {2A3FA00B-353E-439C-958F-1FB5CAE9CD02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2A3FA00B-353E-439C-958F-1FB5CAE9CD02}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2A3FA00B-353E-439C-958F-1FB5CAE9CD02}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2A3FA00B-353E-439C-958F-1FB5CAE9CD02}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {C05D71BB-4F4B-4A73-8E47-44CFA0D6A13F} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clientapp2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aspnet/signalr": "^1.1.4", 7 | "@material-ui/core": "^4.9.10", 8 | "@material-ui/icons": "^4.9.1", 9 | "@testing-library/jest-dom": "^4.2.4", 10 | "@testing-library/react": "^9.4.1", 11 | "@testing-library/user-event": "^7.2.1", 12 | "@types/jest": "^24.9.1", 13 | "@types/node": "^12.12.29", 14 | "@types/react": "^16.9.23", 15 | "@types/react-dom": "^16.9.5", 16 | "@types/react-router-dom": "^5.1.3", 17 | "react": "^16.13.0", 18 | "react-dom": "^16.13.0", 19 | "react-flip-move": "^3.0.4", 20 | "react-markdown": "^4.3.1", 21 | "react-router-dom": "^5.1.2", 22 | "react-scripts": "3.4.0", 23 | "typescript": "^3.7.5" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FTWinston/RobotInterrogation/9ba228ac46c5cc1d5b21337c38650e124330e006/RobotInterrogation/ClientApp/public/favicon.ico -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Robot Interrogation 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/Connectivity.ts: -------------------------------------------------------------------------------- 1 | import * as signalR from '@aspnet/signalr'; 2 | 3 | export async function queryString(url: string) { 4 | const response = await fetch(url, { credentials: 'same-origin' }); 5 | return await response.text(); 6 | } 7 | 8 | export async function queryJson(url: string) { 9 | const response = await fetch(url, { credentials: 'same-origin' }); 10 | const json = await response.json(); 11 | return json as TResponse; 12 | } 13 | 14 | export function connectSignalR(url: string) { 15 | const transportType = inCompatibilityMode() 16 | ? signalR.HttpTransportType.LongPolling 17 | : signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.ServerSentEvents | signalR.HttpTransportType.LongPolling 18 | 19 | return new signalR.HubConnectionBuilder() 20 | .withUrl(url, transportType) 21 | .build(); 22 | } 23 | 24 | export function setCompatibilityMode(enabled: boolean) { 25 | localStorage.setItem('compatiblity', enabled ? '1' : '0'); 26 | } 27 | 28 | export function inCompatibilityMode() { 29 | return localStorage.getItem('compatiblity') === '1'; 30 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/About.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { inCompatibilityMode, setCompatibilityMode } from 'src/Connectivity'; 4 | import { useState } from 'react'; 5 | import { Typography, Button, Link as A, makeStyles } from '@material-ui/core'; 6 | import { ActionSet } from './interviewParts/elements/ActionSet'; 7 | import { Page } from './interviewParts/elements/Page'; 8 | import { P } from './interviewParts/elements/P'; 9 | 10 | const useStyles = makeStyles(theme => ({ 11 | compatibility: { 12 | marginTop: '1.5em', 13 | }, 14 | })); 15 | 16 | export const About: React.FunctionComponent = () => { 17 | const [toggle, setToggle] = useState(false); 18 | 19 | const toggleCompatibility = () => { 20 | setCompatibilityMode(!inCompatibilityMode()); 21 | setToggle(!toggle); 22 | } 23 | 24 | const toggleText = inCompatibilityMode() 25 | ? 'Disable compatibility mode' 26 | : 'Enable compatibility mode'; 27 | 28 | const classes = useStyles(); 29 | 30 | return ( 31 | 32 | Robot Interrogation 33 | 34 |

This game is a conversation between two players, an Interviewer and a Suspect. It should either by two people in the same room, or over third-party video chat.

35 | 36 |

The Suspect must convince the Interviewer that they are human. The Interviewer must determine whether they are a robot. Robots have strange personality quirks, but then so do humans under pressure...

37 | 38 |

This is an online version of Inhuman Conditions, a game by Tommy Maranges and Cory O'Brien which is available for free under Creative Commons license BY-NCA-SA-4.0.

39 | 40 |

Inhuman Conditions' rules are available here. Read them before you play.

41 | 42 |

If you're interested, you can view the source of this project on GitHub. Report any problems there.

43 | 44 | 45 | 46 | 47 | 48 | 49 | Connection problems?  50 | 51 | 52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Typography, Button, Link as A } from '@material-ui/core'; 4 | import { ActionSet } from './interviewParts/elements/ActionSet'; 5 | import { Page } from './interviewParts/elements/Page'; 6 | import { P } from './interviewParts/elements/P'; 7 | 8 | export const Home: React.FunctionComponent = () => { 9 | return ( 10 | 11 | Robot Interrogation 12 | 13 |

Can you tell if someone is secretly a robot? This is an online version of Inhuman Conditions, a game by Tommy Maranges and Cory O'Brien.

14 | 15 |

Play with a friend over video chat, or in the same room.

16 | 17 | 18 | 19 | 20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/Host.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Redirect } from 'react-router'; 3 | import { queryString } from '../Connectivity'; 4 | import { useState, useEffect } from 'react'; 5 | 6 | export const Host: React.FunctionComponent = () => { 7 | const [interviewId, setInterviewId] = useState(); 8 | 9 | useEffect( 10 | () => { 11 | const query = async () => { 12 | const id = await queryString('/api/Data/GetNextSessionID') 13 | setInterviewId(id); 14 | } 15 | query(); 16 | }, 17 | [] 18 | ); 19 | 20 | if (interviewId !== undefined) { 21 | return 22 | } 23 | 24 | return ( 25 |
26 | Please wait... 27 |
28 | ); 29 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/Interview.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Redirect, RouteComponentProps } from 'react-router'; 3 | import { Disconnected } from './interviewParts/Disconnected'; 4 | import { InterviewerInProgress } from './interviewParts/InterviewerInProgress'; 5 | import { InterviewerPenaltySelection } from './interviewParts/InterviewerPenaltySelection'; 6 | import { InterviewerPositionSelection } from './interviewParts/InterviewerPositionSelection'; 7 | import { InterviewerReadyToStart } from './interviewParts/InterviewerReadyToStart'; 8 | import { InterviewFinished } from './interviewParts/InterviewFinished'; 9 | import { NotYetConnected } from './interviewParts/NotYetConnected'; 10 | import { OpponentDisconnected } from './interviewParts/OpponentDisconnected'; 11 | import { InducerWait } from './interviewParts/InducerWait'; 12 | import { PacketSelection } from './interviewParts/PacketSelection'; 13 | import { PenaltyCalibration } from './interviewParts/PenaltyCalibration'; 14 | import { SuspectInProgress } from './interviewParts/SuspectInProgress'; 15 | import { SuspectBackgroundSelection } from './interviewParts/SuspectBackgroundSelection'; 16 | import { SuspectPenaltySelection } from './interviewParts/SuspectPenaltySelection'; 17 | import { SuspectReadyToStart } from './interviewParts/SuspectReadyToStart'; 18 | import { WaitPacketSelection } from './interviewParts/WaitPacketSelection'; 19 | import { WaitingForOpponent } from './interviewParts/WaitingForOpponent'; 20 | import { WaitingQuestionDisplay } from './interviewParts/WaitingQuestionDisplay'; 21 | import { InterviewStatus, InterviewOutcome, interviewReducer, initialState, InterviewPosition } from './interviewReducer'; 22 | import { useReducer, useEffect, useState } from 'react'; 23 | import { connectInterview } from './connectInterview'; 24 | import { InducerPrompt } from './interviewParts/InducerPrompt'; 25 | import { InducerResponse } from './interviewParts/InducerResponse'; 26 | import { InducerDisplay } from './interviewParts/InducerDisplay'; 27 | import { PositionSelection } from './interviewParts/PositionSelection'; 28 | import { WaitPenalty } from './interviewParts/WaitPenalty'; 29 | import { SpectatorPenaltySelection } from './interviewParts/SpectatorPenaltySelection'; 30 | import { SpectatorInducerDisplay } from './interviewParts/SpectatorInducerDisplay'; 31 | import { SpectatorBackgroundSelection } from './interviewParts/SpectatorBackgroundSelection'; 32 | import { SpectatorReadyToStart } from './interviewParts/SpectatorReadyToStart'; 33 | import { SpectatorInProgress } from './interviewParts/SpectatorInProgress'; 34 | 35 | export const Interview: React.FunctionComponent> = props => { 36 | const [state, dispatch] = useReducer(interviewReducer, initialState); 37 | 38 | const [connection, setConnection] = useState(); 39 | 40 | useEffect( 41 | () => { 42 | const connect = async () => { 43 | const newConnection = await connectInterview(props.match.params.id, dispatch); 44 | setConnection(newConnection); 45 | } 46 | 47 | connect(); 48 | }, 49 | [props.match.params.id] 50 | ); 51 | 52 | switch (state.status) { 53 | case InterviewStatus.InvalidSession: 54 | return 55 | 56 | case InterviewStatus.NotConnected: 57 | return 58 | 59 | case InterviewStatus.Disconnected: 60 | return 61 | 62 | case InterviewStatus.WaitingForOpponent: 63 | return 64 | 65 | case InterviewStatus.SelectingPositions: 66 | switch (state.position) { 67 | case InterviewPosition.Interviewer: 68 | const confirm = () => connection!.invoke('ConfirmPositions'); 69 | const swap = () => connection!.invoke('SwapPositions'); 70 | 71 | return 72 | case InterviewPosition.Suspect: 73 | case InterviewPosition.Spectator: 74 | return 75 | } 76 | break; 77 | 78 | case InterviewStatus.PenaltySelection: 79 | if (state.position === InterviewPosition.Spectator) 80 | return 81 | 82 | if (state.choice.length > 0) { 83 | const selectPenalty = (index: number) => connection!.invoke('Select', index); 84 | 85 | return state.position === InterviewPosition.Interviewer 86 | ? 87 | : 88 | } 89 | 90 | return 91 | 92 | case InterviewStatus.PenaltyCalibration: 93 | const confirmPenalty = state.position === InterviewPosition.Interviewer 94 | ? () => connection!.invoke('Select', 0) 95 | : undefined; 96 | 97 | return ( 98 | 103 | ); 104 | 105 | case InterviewStatus.PacketSelection: 106 | const selectPacket = (index: number) => connection!.invoke('Select', index); 107 | 108 | return state.position === InterviewPosition.Interviewer 109 | ? 110 | : 111 | 112 | case InterviewStatus.InducerPrompt: 113 | const administer = () => connection?.invoke('Select', 0); 114 | 115 | switch (state.position) { 116 | case InterviewPosition.Interviewer: 117 | return 122 | 123 | case InterviewPosition.Suspect: 124 | return 128 | 129 | case InterviewPosition.Spectator: 130 | return 139 | } 140 | break; 141 | 142 | case InterviewStatus.ShowingInducer: 143 | const correctResponse = () => connection!.invoke('Select', 0); 144 | const incorrectResponse = () => connection!.invoke('Select', 1); 145 | 146 | switch (state.position) { 147 | case InterviewPosition.Interviewer: 148 | return 154 | 155 | case InterviewPosition.Suspect: 156 | return 164 | 165 | case InterviewPosition.Spectator: 166 | return 175 | } 176 | break; 177 | 178 | case InterviewStatus.BackgroundSelection: 179 | switch (state.position) { 180 | case InterviewPosition.Interviewer: 181 | return dispatch({ 184 | type: 'set questions', 185 | questions, 186 | })} 187 | /> 188 | 189 | case InterviewPosition.Suspect: 190 | const selectBackground = (index: number) => connection!.invoke('Select', index); 191 | return 192 | 193 | case InterviewPosition.Spectator: 194 | return 195 | } 196 | break; 197 | 198 | case InterviewStatus.ReadyToStart: 199 | switch (state.position) { 200 | case InterviewPosition.Interviewer: 201 | const ready = () => connection!.invoke('StartInterview'); 202 | return ( 203 | dispatch({ 210 | type: 'set questions', 211 | questions, 212 | })} 213 | /> 214 | ); 215 | 216 | case InterviewPosition.Suspect: 217 | return ( 218 | 223 | ); 224 | 225 | case InterviewPosition.Spectator: 226 | return ( 227 | 232 | ); 233 | } 234 | break; 235 | 236 | case InterviewStatus.InProgress: 237 | switch (state.position) { 238 | case InterviewPosition.Interviewer: 239 | const conclude = (isRobot: boolean) => connection!.invoke('ConcludeInterview', isRobot); 240 | return ( 241 | 248 | ); 249 | 250 | case InterviewPosition.Suspect: 251 | const terminate = () => connection!.invoke('KillInterviewer'); 252 | return ( 253 | 260 | ); 261 | 262 | case InterviewPosition.Spectator: 263 | return ( 264 | 270 | ); 271 | } 272 | break; 273 | 274 | case InterviewStatus.Finished: 275 | if (state.outcome! === InterviewOutcome.Disconnected) { 276 | return 277 | } 278 | 279 | const playAgain = () => connection!.invoke('NewInterview'); 280 | 281 | return ( 282 | 288 | ); 289 | } 290 | 291 | return
Unknown status
292 | } 293 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/connectInterview.ts: -------------------------------------------------------------------------------- 1 | import { connectSignalR } from 'src/Connectivity'; 2 | import { Dispatch } from 'react'; 3 | import { InterviewAction, InterviewOutcome, IPacket } from './interviewReducer'; 4 | import { ISuspectRole } from './interviewParts/elements/SuspectRole'; 5 | import { IInterviewQuestion } from './interviewParts/elements/InterviewQuestion'; 6 | 7 | export async function connectInterview(session: string, dispatch: Dispatch) { 8 | const connection = connectSignalR('/hub/Interview'); 9 | 10 | connection.on('SetPosition', (position: number) => { 11 | dispatch({ 12 | type: 'set position', 13 | position, 14 | }); 15 | }); 16 | 17 | connection.on('SetWaitingForPlayer', () => { 18 | dispatch({ 19 | type: 'set waiting for opponent', 20 | }); 21 | }); 22 | 23 | connection.on('SetPlayersPresent', () => { 24 | dispatch({ 25 | type: 'set selecting positions', 26 | }); 27 | }); 28 | 29 | connection.on('SwapPositions', () => { 30 | dispatch({ 31 | type: 'swap position' 32 | }); 33 | }); 34 | 35 | connection.on('ShowPenaltyChoice', (options: string[]) => { 36 | dispatch({ 37 | type: 'set penalty choice', 38 | options, 39 | }); 40 | }); 41 | 42 | connection.on('WaitForPenaltyChoice', () => { 43 | dispatch({ 44 | type: 'set penalty choice', 45 | options: [], 46 | }); 47 | }); 48 | 49 | connection.on('SpectatorWaitForPenaltyChoice', (options: string[], turn: number) => { 50 | dispatch({ 51 | type: 'set penalty choice', 52 | options, 53 | turn 54 | }); 55 | }); 56 | 57 | connection.on('SetPenalty', (penalty: string) => { 58 | dispatch({ 59 | type: 'set penalty', 60 | penalty, 61 | }); 62 | }); 63 | 64 | connection.on('ShowPacketChoice', (options: IPacket[]) => { 65 | dispatch({ 66 | type: 'set packet choice', 67 | options, 68 | }); 69 | }); 70 | 71 | connection.on('WaitForPacketChoice', () => { 72 | dispatch({ 73 | type: 'set packet choice', 74 | options: [], 75 | }); 76 | }); 77 | 78 | connection.on('SetPacket', (packet: string, prompt: string) => { 79 | dispatch({ 80 | type: 'set packet', 81 | packet, 82 | prompt, 83 | }); 84 | }); 85 | 86 | connection.on('ShowInducerPrompt', (solution: string[]) => { 87 | dispatch({ 88 | type: 'prompt inducer', 89 | solution, 90 | }); 91 | }); 92 | 93 | connection.on('WaitForInducer', () => { 94 | dispatch({ 95 | type: 'prompt inducer', 96 | solution: [] 97 | }); 98 | }); 99 | 100 | connection.on('SpectatorWaitForInducer', (role: ISuspectRole, solution: string[]) => { 101 | dispatch({ 102 | type: 'spectator wait inducer', 103 | role, 104 | solution, 105 | }); 106 | }); 107 | 108 | connection.on('SpectatorWaitForInducer', (role: ISuspectRole, solution: string[], connections: number[][], contents: string[][]) => { 109 | dispatch({ 110 | type: 'spectator wait inducer', 111 | role, 112 | solution, 113 | patternConnections: connections, 114 | patternContent: contents, 115 | }); 116 | }); 117 | 118 | connection.on('SetRoleWithPattern', (role: ISuspectRole, connections: number[][], contents: string[][]) => { 119 | dispatch({ 120 | type: 'set role and pattern', 121 | role, 122 | patternConnections: connections, 123 | patternContent: contents, 124 | }); 125 | }); 126 | 127 | connection.on('SetRoleWithSolution', (role: ISuspectRole, solution: string[]) => { 128 | dispatch({ 129 | type: 'set role and solution', 130 | role, 131 | solution, 132 | }); 133 | }); 134 | 135 | connection.on('SetQuestions', (questions: IInterviewQuestion[]) => { 136 | dispatch({ 137 | type: 'set questions', 138 | questions, 139 | }); 140 | }); 141 | 142 | connection.on('ShowInducer', () => { 143 | dispatch({ 144 | type: 'show inducer', 145 | }); 146 | }); 147 | 148 | connection.on('ShowSuspectBackgroundChoice', (options: string[]) => { 149 | dispatch({ 150 | type: 'set background choice', 151 | options, 152 | }); 153 | }); 154 | 155 | connection.on('WaitForSuspectBackgroundChoice', () => { 156 | dispatch({ 157 | type: 'set background choice', 158 | options: [], 159 | }); 160 | }); 161 | 162 | connection.on('SpectatorWaitForSuspectBackgroundChoice', (options: string[]) => { 163 | dispatch({ 164 | type: 'set background choice', 165 | options, 166 | }); 167 | }); 168 | 169 | connection.on('SetSuspectBackground', (note: string) => { 170 | dispatch({ 171 | type: 'set background', 172 | background: note, 173 | }); 174 | }); 175 | 176 | connection.on('StartTimer', (duration: number) => { 177 | dispatch({ 178 | type: 'start timer', 179 | duration, 180 | }); 181 | }) 182 | 183 | connection.on('EndGame', (outcome: InterviewOutcome, role: ISuspectRole) => { 184 | dispatch({ 185 | type: 'end game', 186 | outcome, 187 | role, 188 | }) 189 | }) 190 | 191 | connection.onclose((error?: Error) => { 192 | /* 193 | if (error !== undefined) { 194 | console.log('Connection error:', error); 195 | } 196 | else { 197 | console.log('Unspecified connection error'); 198 | } 199 | */ 200 | 201 | dispatch({ 202 | type: 'disconnect', 203 | }); 204 | }); 205 | 206 | await connection.start(); 207 | 208 | const ok = await connection.invoke('Join', session) 209 | 210 | if (!ok) { 211 | dispatch({ 212 | type: 'invalid session', 213 | }); 214 | 215 | await connection.stop(); 216 | } 217 | 218 | return connection; 219 | } 220 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/Disconnected.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { ActionSet } from './elements/ActionSet'; 4 | import { P } from './elements/P'; 5 | import { Button, Typography } from '@material-ui/core'; 6 | import { Page } from './elements/Page'; 7 | 8 | export const Disconnected: React.FunctionComponent = () => { 9 | return ( 10 | 11 | 12 |

You have disconnected from the interview.

13 |

Check your internet connection.

14 | 15 | 16 | 17 | 18 |
19 | ); 20 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/GameLink.css: -------------------------------------------------------------------------------- 1 | .gameLink__focus { 2 | font-size: 1.5em; 3 | } 4 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/InducerDisplay.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition, Direction } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 5 | import { InterferenceSolution } from './elements/InterferenceSolution'; 6 | import { InterferencePattern } from './elements/InterferencePattern'; 7 | import { PacketDisplay } from './elements/PacketDisplay'; 8 | import { Page } from './elements/Page'; 9 | import { P } from './elements/P'; 10 | import { Typography } from '@material-ui/core'; 11 | import { Help } from './elements/Help'; 12 | 13 | interface IProps { 14 | position: InterviewPosition; 15 | packet: string; 16 | role: ISuspectRole; 17 | connections?: Direction[][]; 18 | content?: string[][]; 19 | solution?: string[]; 20 | } 21 | 22 | export const InducerDisplay: React.FunctionComponent = props => { 23 | const patternOrSolution = props.solution !== undefined 24 | ? 25 | : props.connections !== undefined && props.content !== undefined 26 | ? 27 | : undefined; 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | The inducer has been administered. Your role: 36 | 37 | 38 | {patternOrSolution} 39 | 40 |

Answer the Interviewer's question based on the above diagram.
If you answer correctly, you can choose your background.

41 |
42 | ) 43 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/InducerPrompt.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { InterferenceSolution } from './elements/InterferenceSolution'; 5 | import { PacketDisplay } from './elements/PacketDisplay'; 6 | import { ActionSet } from './elements/ActionSet'; 7 | import { Help } from './elements/Help'; 8 | import { Page } from './elements/Page'; 9 | import { P } from './elements/P'; 10 | import { Button, Typography } from '@material-ui/core'; 11 | 12 | interface IProps { 13 | solution: string[]; 14 | packet: string; 15 | continue: () => void; 16 | } 17 | 18 | export const InducerPrompt: React.FunctionComponent = props => { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 |

Ask the Suspect a question based on the sequence below, then click below to administer the inducer, revealing the Suspect's role to them.

26 | 27 | 28 | 29 | 30 | Example questions: 31 |
    32 |
  • What letters come between A and D?
  • 33 |
  • What letter follows B?
  • 34 |
35 |
36 | 37 | 38 | 39 | 40 |
41 | ) 42 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/InducerResponse.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { InterferenceSolution } from './elements/InterferenceSolution'; 5 | import { PacketDisplay } from './elements/PacketDisplay'; 6 | import { ActionSet } from './elements/ActionSet'; 7 | import { Page } from './elements/Page'; 8 | import { P } from './elements/P'; 9 | import { Button } from '@material-ui/core'; 10 | import { Help } from './elements/Help'; 11 | 12 | interface IProps { 13 | solution: string[]; 14 | packet: string; 15 | correct: () => void; 16 | incorrect: () => void; 17 | } 18 | 19 | export const InducerResponse: React.FunctionComponent = props => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 |

The inducer has been administered.

27 | 28 | 29 | 30 |

Wait for the Suspect to answer your question, then indicate whether their response is correct. If they are correct, they can choose their background.

31 | 32 | 33 | 34 | 35 | 36 |
37 | ) 38 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/InducerWait.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { PacketDisplay } from './elements/PacketDisplay'; 5 | import { Page } from './elements/Page'; 6 | import { P } from './elements/P'; 7 | import { Help } from './elements/Help'; 8 | 9 | interface IProps { 10 | position: InterviewPosition; 11 | packet: string; 12 | } 13 | 14 | export const InducerWait: React.FunctionComponent = props => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 |

The Interviewer will ask you a question, and then administer the inducer.

22 |

You will see your role for this interview, and a diagram you will need to answer the Interviewer's question.

23 |

Answer the question correctly to be able to choose your background.

24 |
25 | ) 26 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/InterviewFinished.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewOutcome, InterviewPosition } from '../interviewReducer'; 3 | import { ISuspectRole } from './elements/SuspectRole'; 4 | import { ActionSet } from './elements/ActionSet'; 5 | import { Page } from './elements/Page'; 6 | import { Button } from '@material-ui/core'; 7 | import { P } from './elements/P'; 8 | import { OutcomeHumanCorrect } from './OutcomeHumanCorrect'; 9 | import { OutcomeRobotCorrect } from './OutcomeRobotCorrect'; 10 | import { OutcomeHumanIncorrect } from './OutcomeHumanIncorrect'; 11 | import { OutcomeRobotIncorrect } from './OutcomeRobotIncorrect'; 12 | import { OutcomeViolentKilled } from './OutcomeViolentKilled'; 13 | 14 | interface IProps { 15 | position: InterviewPosition; 16 | role: ISuspectRole; 17 | outcome: InterviewOutcome; 18 | playAgain: () => void; 19 | } 20 | 21 | export const InterviewFinished: React.FunctionComponent = props => { 22 | const playAgain = props.position === InterviewPosition.Interviewer || props.position === InterviewPosition.Suspect 23 | ? 24 | 25 | 26 | : undefined; 27 | 28 | function renderOutcome() { 29 | switch (props.outcome) { 30 | case InterviewOutcome.CorrectlyGuessedHuman: 31 | return ; 32 | 33 | case InterviewOutcome.CorrectlyGuessedRobot: 34 | return ; 35 | 36 | case InterviewOutcome.WronglyGuessedHuman: 37 | return ; 38 | 39 | case InterviewOutcome.WronglyGuessedRobot: 40 | return ; 41 | 42 | case InterviewOutcome.KilledInterviewer: 43 | return ; 44 | 45 | default: 46 | return

Unknown outcome

; 47 | } 48 | } 49 | 50 | return ( 51 | 52 | {renderOutcome()} 53 | {playAgain} 54 | 55 | ); 56 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/InterviewerInProgress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Countdown } from './elements/Countdown'; 3 | import { IInterviewQuestion, InterviewQuestion } from './elements/InterviewQuestion'; 4 | import { useState } from 'react'; 5 | import { PositionHeader } from './elements/PositionHeader'; 6 | import { InterviewPosition } from '../interviewReducer'; 7 | import { ActionSet } from './elements/ActionSet'; 8 | import { Button } from '@material-ui/core'; 9 | import { P } from './elements/P'; 10 | import { Page } from './elements/Page'; 11 | 12 | interface IProps { 13 | questions: IInterviewQuestion[]; 14 | suspectBackground: string; 15 | penalty: string; 16 | duration: number; 17 | conclude: (isRobot: boolean) => void; 18 | } 19 | 20 | export const InterviewerInProgress: React.FunctionComponent = props => { 21 | const questions = props.questions.map((q, i) => ); 22 | 23 | const isHuman = () => props.conclude(false); 24 | const isRobot = () => props.conclude(true); 25 | 26 | const [elapsed, setElapsed] = useState(false); 27 | 28 | const elapsedPrompt = elapsed 29 | ?

You can ask one final question.

30 | :

; 31 | 32 | return 33 | 34 |

Ask the Suspect questions and decide whether they are human or a robot.

35 | 36 |
37 | {questions} 38 |
39 | 40 |

Penalty: {props.penalty}

41 | 42 |

Suspect background: {props.suspectBackground}

43 | 44 | setElapsed(true)} 47 | /> 48 | 49 | {elapsedPrompt} 50 | 51 | 52 | 60 | 66 | 67 | 68 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/InterviewerPenaltySelection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { PositionHeader } from './elements/PositionHeader'; 3 | import { InterviewPosition } from '../interviewReducer'; 4 | import { Page } from './elements/Page'; 5 | import { P } from './elements/P'; 6 | import { Help } from './elements/Help'; 7 | import { ChoiceArray } from './elements/ChoiceArray'; 8 | 9 | interface IProps { 10 | options: string[], 11 | action: (index: number) => void, 12 | } 13 | 14 | export const InterviewerPenaltySelection: React.FunctionComponent = props => { 15 | return ( 16 | 17 | 18 |

Select one of the following penalties to discard. The Suspect will choose from the remaining two.

19 | 20 | 21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/InterviewerPositionSelection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { PositionHeader } from './elements/PositionHeader'; 3 | import { InterviewPosition } from '../interviewReducer'; 4 | import { Button } from '@material-ui/core'; 5 | import { Help } from './elements/Help'; 6 | import { Page } from './elements/Page'; 7 | import { P } from './elements/P'; 8 | import { Choice } from './elements/Choice'; 9 | 10 | interface IProps { 11 | stay: () => void, 12 | swap: () => void, 13 | } 14 | 15 | export const InterviewerPositionSelection: React.FunctionComponent = props => { 16 | return ( 17 | 18 | 19 | 20 |

21 | Select your position for this interview: 22 |

23 | 24 | 25 | 26 | 27 | 28 |
29 | ); 30 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/InterviewerReadyToStart.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IInterviewQuestion } from './elements/InterviewQuestion'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { InterviewPosition } from '../interviewReducer'; 5 | import { SortableQuestions } from './elements/SortableQuestions'; 6 | import { ActionSet } from './elements/ActionSet'; 7 | import { Page } from './elements/Page'; 8 | import { P } from './elements/P'; 9 | import { Button } from '@material-ui/core'; 10 | import { Help } from './elements/Help'; 11 | 12 | interface IProps { 13 | prompt: string, 14 | questions: IInterviewQuestion[], 15 | suspectBackground: string, 16 | penalty: string, 17 | ready: () => void, 18 | sortQuestions: (questions: IInterviewQuestion[]) => void; 19 | } 20 | 21 | export const InterviewerReadyToStart: React.FunctionComponent = props => { 22 | return ( 23 | 24 | 25 |

Ask the Suspect their name, then confirm their background.
When you are ready, read them the prompt, then start the timer.

26 | 27 | 31 | 32 |

Penalty: {props.penalty}

33 |

Suspect background: {props.suspectBackground}

34 | 35 |

Prompt: {props.prompt}

36 | 37 | 38 | 39 | 40 |
41 | ); 42 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/NotYetConnected.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Page } from './elements/Page'; 3 | import { P } from './elements/P'; 4 | import { Typography } from '@material-ui/core'; 5 | 6 | export const NotYetConnected: React.FunctionComponent = () => { 7 | return ( 8 | 9 | 10 |

Connecting...

11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/OpponentDisconnected.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { ActionSet } from './elements/ActionSet'; 4 | import { Page } from './elements/Page'; 5 | import { P } from './elements/P'; 6 | import { Button, Typography } from '@material-ui/core'; 7 | 8 | export const OpponentDisconnected: React.FunctionComponent = () => { 9 | return ( 10 | 11 | 12 |

Your opponent disconnected from the interview.

13 | 14 | 15 | 16 | 17 |
18 | ); 19 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/OutcomeHumanCorrect.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition } from '../interviewReducer'; 3 | import { Typography } from '@material-ui/core'; 4 | import { Page } from './elements/Page'; 5 | import { P } from './elements/P'; 6 | 7 | interface IProps { 8 | position: InterviewPosition; 9 | } 10 | 11 | export const OutcomeHumanCorrect: React.FunctionComponent = (props) => { 12 | switch (props.position) { 13 | case InterviewPosition.Interviewer: 14 | return ( 15 | 16 |

You correctly identified the suspect as a human.

17 | You both win. 18 |
19 | ); 20 | 21 | case InterviewPosition.Suspect: 22 | return ( 23 | 24 |

The interviewer correctly identified you as a human.

25 | You both win. 26 |
27 | ); 28 | 29 | default: 30 | return ( 31 | 32 |

The interviewer correctly identified the suspect as a human.

33 | They both win. 34 |
35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/OutcomeHumanIncorrect.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 3 | import { Typography } from '@material-ui/core'; 4 | import { P } from './elements/P'; 5 | import { Page } from './elements/Page'; 6 | import { InterviewPosition } from '../interviewReducer'; 7 | 8 | interface IProps { 9 | position: InterviewPosition, 10 | role: ISuspectRole, 11 | } 12 | 13 | export const OutcomeHumanIncorrect: React.FunctionComponent = props => { 14 | switch (props.position) { 15 | case InterviewPosition.Interviewer: 16 | let winOrLose = props.role.type === 'ViolentRobot' 17 | ? You both lose. 18 | : You lose. 19 | 20 | let extra = props.role.type === 'ViolentRobot' 21 | ?

(Violent robots cannot win by being certified as human. They only win by completing their tasks.)

22 | : undefined; 23 | 24 | return ( 25 | 26 |

You wrongly identified the suspect as a human.
They are actually a robot.

27 | {winOrLose} 28 | 29 | {extra} 30 |
31 | ); 32 | 33 | case InterviewPosition.Suspect: 34 | winOrLose = props.role.type === 'ViolentRobot' 35 | ? You both lose. 36 | : You win. 37 | 38 | extra = props.role.type === 'ViolentRobot' 39 | ?

(Violent robots cannot win by being certified as human. They only win by completing their tasks.)

40 | : undefined; 41 | 42 | return ( 43 | 44 |

The interviewer wrongly identified you as a human.

45 | {winOrLose} 46 | 47 | {extra} 48 |
49 | ); 50 | 51 | default: 52 | winOrLose = props.role.type === 'ViolentRobot' 53 | ? They both lose. 54 | : Suspect wins. 55 | 56 | extra = props.role.type === 'ViolentRobot' 57 | ?

(Violent robots cannot win by being certified as human. They only win by completing their tasks.)

58 | : undefined; 59 | 60 | return ( 61 | 62 |

The interviewer wrongly identified the suspect as a human.

63 | {winOrLose} 64 | 65 | {extra} 66 |
67 | ); 68 | } 69 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/OutcomeRobotCorrect.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 3 | import { Page } from './elements/Page'; 4 | import { P } from './elements/P'; 5 | import { Typography } from '@material-ui/core'; 6 | import { InterviewPosition } from '../interviewReducer'; 7 | 8 | interface IProps { 9 | position: InterviewPosition, 10 | role: ISuspectRole, 11 | } 12 | 13 | export const OutcomeRobotCorrect: React.FunctionComponent = (props) => { 14 | switch (props.position) { 15 | case InterviewPosition.Interviewer: 16 | return ( 17 | 18 |

You correctly identified the suspect as a robot.

19 | You win. 20 | 21 |
22 | ); 23 | 24 | case InterviewPosition.Suspect: 25 | return ( 26 | 27 |

The Interviewer correctly identified you as a robot.

28 | You lose. 29 | 30 |
31 | ); 32 | 33 | default: 34 | return ( 35 | 36 |

The Interviewer correctly identified the suspect as a robot.

37 | Interviewer wins. 38 | 39 |
40 | ); 41 | } 42 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/OutcomeRobotIncorrect.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | import { Page } from './elements/Page'; 4 | import { P } from './elements/P'; 5 | import { InterviewPosition } from '../interviewReducer'; 6 | 7 | interface IProps { 8 | position: InterviewPosition; 9 | } 10 | 11 | export const OutcomeRobotIncorrect: React.FunctionComponent = (props) => { 12 | switch (props.position) { 13 | case InterviewPosition.Interviewer: 14 | return ( 15 | 16 |

You wrongly identified the suspect as a robot.
They are actually human.

17 | You both lose. 18 |
19 | ); 20 | 21 | case InterviewPosition.Suspect: 22 | return ( 23 | 24 |

The Interviewer wrongly identified you as a robot.

25 | You both lose. 26 |
27 | ); 28 | 29 | default: 30 | return ( 31 | 32 |

The Interviewer wrongly identified the suspect as a robot.

33 | They both lose. 34 |
35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/OutcomeViolentKilled.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 3 | import { Typography } from '@material-ui/core'; 4 | import { Page } from './elements/Page'; 5 | import { P } from './elements/P'; 6 | import { InterviewPosition } from '../interviewReducer'; 7 | 8 | interface IProps { 9 | position: InterviewPosition; 10 | role: ISuspectRole; 11 | } 12 | 13 | export const OutcomeViolentKilled: React.FunctionComponent = props => { 14 | switch (props.position) { 15 | case InterviewPosition.Interviewer: 16 | return ( 17 | 18 |

The suspect was a violent robot who completed their obsession and killed you.

19 | You lose. 20 | 21 | 22 |
23 | ); 24 | 25 | case InterviewPosition.Suspect: 26 | return ( 27 | 28 |

You completed your obsession and killed the Interviewer.

29 | You win. 30 | 31 | 32 |
33 | ); 34 | 35 | default: 36 | return ( 37 | 38 |

Suspect completed their obsession and killed the Interviewer.

39 | Suspect wins. 40 | 41 | 42 |
43 | ); 44 | } 45 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/PacketSelection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PositionHeader } from './elements/PositionHeader'; 3 | import { InterviewPosition, IPacket } from '../interviewReducer'; 4 | import { P } from './elements/P'; 5 | import { Page } from './elements/Page'; 6 | import { Help } from './elements/Help'; 7 | import Phone from '@material-ui/icons/Phone'; 8 | import Transform from '@material-ui/icons/Transform'; 9 | import WbIncandescent from '@material-ui/icons/WbIncandescent'; 10 | import People from '@material-ui/icons/People'; 11 | import Favorite from '@material-ui/icons/Favorite'; 12 | import SentimentVeryDissatisfied from '@material-ui/icons/SentimentVeryDissatisfied'; 13 | import Warning from '@material-ui/icons/Warning'; 14 | import LocalBar from '@material-ui/icons/LocalBar'; 15 | import TransferWithinAStation from '@material-ui/icons/TransferWithinAStation'; 16 | import Portrait from '@material-ui/icons/Portrait'; 17 | import BeachAccess from '@material-ui/icons/BeachAccess'; 18 | import HelpOutline from '@material-ui/icons/HelpOutline'; 19 | import { List, ListItemIcon, ListItem, ListItemText } from '@material-ui/core'; 20 | 21 | interface IProps { 22 | options: IPacket[], 23 | action: (index: number) => void, 24 | } 25 | 26 | export const PacketSelection: React.FunctionComponent = props => { 27 | const options = props.options.map((packet, index) => ( 28 | props.action(index)}> 29 | 30 | {getIcon(packet.icon)} 31 | 32 | 33 | 34 | )); 35 | 36 | return ( 37 | 38 | 39 |

Please select an interview packet to use for this interview.

40 | 41 | {options} 42 |
43 | ) 44 | } 45 | 46 | function getIcon(name: string) { 47 | switch (name) { 48 | case 'phone': 49 | return 50 | case 'problem': 51 | return 52 | case 'imagine': 53 | return 54 | case 'cooperate': 55 | return 56 | case 'dream': 57 | return 58 | case 'body': 59 | return 60 | case 'grief': 61 | return 62 | case 'threat': 63 | return 64 | case 'moral': 65 | return 66 | case 'self': 67 | return 68 | case 'intent': 69 | return 70 | default: 71 | return 72 | } 73 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/PenaltyCalibration.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { ActionSet } from './elements/ActionSet'; 5 | import { Page } from './elements/Page'; 6 | import { Button } from '@material-ui/core'; 7 | import { P } from './elements/P'; 8 | import { Help } from './elements/Help'; 9 | import { ValueDisplay } from './elements/ValueDisplay'; 10 | 11 | interface IProps { 12 | position: InterviewPosition; 13 | penalty: string; 14 | confirm?: () => void; 15 | } 16 | 17 | export const PenaltyCalibration: React.FunctionComponent = props => { 18 | const extraMessage = [ 19 |

Ask the Suspect to perform this penalty 3 times.
When you are satisfied that they have done so, click to continue.

, 20 |

The Interviewer will now ask you to perform this penalty 3 times.

, 21 |

Waiting for the Suspect to perform this penalty 3 times.

22 | ][props.position]; 23 | 24 | const confirm = props.confirm 25 | ? ( 26 | 27 | 28 | 29 | ) 30 | : undefined; 31 | 32 | return ( 33 | 34 | 35 | 36 | The chosen penalty is: 37 | 38 | {extraMessage} 39 | {confirm} 40 | 41 | ) 42 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/PositionSelection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { Help } from './elements/Help'; 5 | import { P } from './elements/P'; 6 | import { Page } from './elements/Page'; 7 | 8 | interface IProps { 9 | position: InterviewPosition; 10 | } 11 | 12 | export const PositionSelection: React.FunctionComponent = props => { 13 | return ( 14 | 15 | 16 |

Waiting for the Interviewer to confirm or swap positions.

17 |
18 | ); 19 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/SpectatorBackgroundSelection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 3 | import { InterviewPosition } from '../interviewReducer'; 4 | import { PositionHeader } from './elements/PositionHeader'; 5 | import { ActionSet } from './elements/ActionSet'; 6 | import { ChoiceArray } from './elements/ChoiceArray'; 7 | import { Button, Typography } from '@material-ui/core'; 8 | import { P } from './elements/P'; 9 | import { Help } from './elements/Help'; 10 | import { Page } from './elements/Page'; 11 | 12 | interface IProps { 13 | options: string[], 14 | role: ISuspectRole, 15 | } 16 | 17 | export const SpectatorBackgroundSelection: React.FunctionComponent = props => { 18 | const message = props.options.length === 1 19 | ?

Suspect answered incorrectly. They can only choose the following background:

20 | :

Suspect answered correctly. They can choose one of the following backgrounds:

21 | 22 | const options = props.options.length === 1 23 | ? ( 24 | 25 | 26 | 27 | ) 28 | : 29 | 30 | return ( 31 | 32 | 33 | 34 | Suspect's role: 35 | 36 | 37 | {message} 38 | 39 | {options} 40 | 41 | ); 42 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/SpectatorInProgress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Countdown } from './elements/Countdown'; 3 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 4 | import { useState } from 'react'; 5 | import { PositionHeader } from './elements/PositionHeader'; 6 | import { InterviewPosition } from '../interviewReducer'; 7 | import { P } from './elements/P'; 8 | import { Page } from './elements/Page'; 9 | 10 | interface IProps { 11 | suspectBackground: string; 12 | penalty: string; 13 | role: ISuspectRole; 14 | duration: number; 15 | } 16 | 17 | export const SpectatorInProgress: React.FunctionComponent = props => { 18 | const robotPrompt = props.role.type === 'ViolentRobot' 19 | ? <> 20 |

Suspect must complete 2 of the 3 tasks listed below, and then wait 10 seconds before they can kill the Interviewer.

21 |

If they cannot kill the Interviewer before finishing answering their final question, they must visibly malfunction.

22 | 23 | : props.role.type === 'PassiveRobot' 24 | ?

Suspect must perform the penalty each time they violate their vulnerability.

25 | : undefined; 26 | 27 | const [elapsed, setElapsed] = useState(false); 28 | 29 | const elapsedPrompt = elapsed 30 | ?

The interviewer can ask one final question.

31 | :

; 32 | 33 | return 34 | 35 |

While answering the Interviewer's questions, the Suspect will try to convince them that they are human.

36 | {robotPrompt} 37 | 38 | 39 | 40 |

Penalty: {props.penalty}

41 | 42 |

Suspect background: {props.suspectBackground}

43 | 44 | setElapsed(true)} 47 | /> 48 | 49 | {elapsedPrompt} 50 | 51 | } 52 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/SpectatorInducerDisplay.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition, Direction } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 5 | import { InterferenceSolution } from './elements/InterferenceSolution'; 6 | import { InterferencePattern } from './elements/InterferencePattern'; 7 | import { PacketDisplay } from './elements/PacketDisplay'; 8 | import { Page } from './elements/Page'; 9 | import { P } from './elements/P'; 10 | import { Typography } from '@material-ui/core'; 11 | import { Help } from './elements/Help'; 12 | 13 | interface IProps { 14 | position: InterviewPosition; 15 | packet: string; 16 | role: ISuspectRole; 17 | shown: boolean; 18 | connections?: Direction[][]; 19 | content?: string[][]; 20 | solution?: string[]; 21 | } 22 | 23 | export const SpectatorInducerDisplay: React.FunctionComponent = props => { 24 | const correctTense = props.shown ? 'has been' : 'is about to be'; 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | The inducer {correctTense} administered.
Suspect's role:
33 | 34 | 35 | {props.solution ? : undefined} 36 | {props.connections !== undefined && props.content !== undefined ? : undefined} 37 | 38 |

The Suspect will now answer the Interviewer's question.
If answered correctly, the Suspect will get to choose their background.

39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/SpectatorPenaltySelection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { Help } from './elements/Help'; 5 | import { P } from './elements/P'; 6 | import { Page } from './elements/Page'; 7 | import { ChoiceArray } from './elements/ChoiceArray'; 8 | 9 | interface IProps { 10 | turn: InterviewPosition, 11 | options: string[], 12 | } 13 | 14 | export const SpectatorPenaltySelection: React.FunctionComponent = props => { 15 | const message = props.turn === InterviewPosition.Interviewer 16 | ?

Waiting for interviewer to discard a penalty.

17 | :

Waiting for suspect to choose a penalty.

18 | 19 | return ( 20 | 21 | 22 | {message} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/SpectatorReadyToStart.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { InterviewPosition } from '../interviewReducer'; 5 | import { Page } from './elements/Page'; 6 | import { P } from './elements/P'; 7 | import { Help } from './elements/Help'; 8 | 9 | interface IProps { 10 | suspectBackground: string, 11 | penalty: string, 12 | role: ISuspectRole, 13 | } 14 | 15 | export const SpectatorReadyToStart: React.FunctionComponent = props => { 16 | const robotPrompt = props.role.type === 'ViolentRobot' 17 | ?

Suspect must complete 2 of the 3 tasks listed below, and then wait 10 seconds before they can kill the Interviewer.

18 | : props.role.type === 'PatientRobot' 19 | ?

Suspect must perform the penalty each time they violate their vulnerability.

20 | : undefined; 21 | 22 | return ( 23 | 24 | 25 |

Once they start the timer, the Interviewer will ask the suspect a series of questions to try to determine whether they are a human or a robot.

26 | 27 | {robotPrompt} 28 | 29 | 30 | 31 |

Penalty: {props.penalty}

32 |

Suspect background: {props.suspectBackground}

33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/SuspectBackgroundSelection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 3 | import { InterviewPosition } from '../interviewReducer'; 4 | import { PositionHeader } from './elements/PositionHeader'; 5 | import { ActionSet } from './elements/ActionSet'; 6 | import { ChoiceArray } from './elements/ChoiceArray'; 7 | import { Button, Typography } from '@material-ui/core'; 8 | import { P } from './elements/P'; 9 | import { Help } from './elements/Help'; 10 | import { Page } from './elements/Page'; 11 | 12 | interface IProps { 13 | options: string[], 14 | role: ISuspectRole; 15 | action: (index: number) => void, 16 | } 17 | 18 | export const SuspectBackgroundSelection: React.FunctionComponent = props => { 19 | const message = props.options.length === 1 20 | ?

You answered incorrectly. You can only choose the following background:

21 | :

You answered correctly. Select one of the following backgrounds:

22 | 23 | const options = props.options.length === 1 24 | ? ( 25 | 26 | 27 | 28 | ) 29 | : 30 | 31 | return ( 32 | 33 | 34 | 35 | Your role: 36 | 37 | 38 | {message} 39 | 40 | {options} 41 | 42 | ); 43 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/SuspectInProgress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Countdown } from './elements/Countdown'; 3 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 4 | import { useState } from 'react'; 5 | import { PositionHeader } from './elements/PositionHeader'; 6 | import { InterviewPosition } from '../interviewReducer'; 7 | import { ActionSet } from './elements/ActionSet'; 8 | import { P } from './elements/P'; 9 | import { Button } from '@material-ui/core'; 10 | import { Page } from './elements/Page'; 11 | 12 | interface IProps { 13 | suspectBackground: string; 14 | penalty: string; 15 | role: ISuspectRole; 16 | duration: number; 17 | terminateInterviewer: () => void; 18 | } 19 | 20 | export const SuspectInProgress: React.FunctionComponent = props => { 21 | const terminate = props.role.type === 'ViolentRobot' 22 | ? ( 23 | 24 | 25 | 26 | ) 27 | : undefined; 28 | 29 | const robotPrompt = props.role.type === 'ViolentRobot' 30 | ? <> 31 |

You must complete 2 of the 3 tasks listed below, and then wait 10 seconds before you can kill the Interviewer.

32 |

If you cannot kill the Interviewer before finishing answering their final question, you must visibly malfunction.

33 | 34 | : props.role.type === 'PassiveRobot' 35 | ?

You must perform the penalty each time you violate your vulnerability.

36 | : undefined; 37 | 38 | const [elapsed, setElapsed] = useState(false); 39 | 40 | const elapsedPrompt = elapsed 41 | ?

The interviewer can ask one final question.

42 | :

; 43 | 44 | return 45 | 46 |

Answer the Interviewer's questions, try to convince them that you are human.

47 | {robotPrompt} 48 | 49 | 50 | 51 |

Penalty: {props.penalty}

52 | 53 |

Your background: {props.suspectBackground}

54 | 55 | setElapsed(true)} 58 | /> 59 | 60 | {elapsedPrompt} 61 | 62 | {terminate} 63 | 64 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/SuspectPenaltySelection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { PositionHeader } from './elements/PositionHeader'; 3 | import { InterviewPosition } from '../interviewReducer'; 4 | import { Help } from './elements/Help'; 5 | import { Page } from './elements/Page'; 6 | import { P } from './elements/P'; 7 | import { ChoiceArray } from './elements/ChoiceArray'; 8 | 9 | interface IProps { 10 | options: string[], 11 | action: (index: number) => void, 12 | } 13 | 14 | export const SuspectPenaltySelection: React.FunctionComponent = props => { 15 | return ( 16 | 17 | 18 |

Select one of the following penalties to use for this interview.

19 | 20 | 21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/SuspectReadyToStart.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ISuspectRole, SuspectRole } from './elements/SuspectRole'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { InterviewPosition } from '../interviewReducer'; 5 | import { Page } from './elements/Page'; 6 | import { P } from './elements/P'; 7 | import { Help } from './elements/Help'; 8 | 9 | interface IProps { 10 | suspectBackground: string, 11 | penalty: string, 12 | role: ISuspectRole, 13 | } 14 | 15 | export const SuspectReadyToStart: React.FunctionComponent = props => { 16 | const robotPrompt = props.role.type === 'ViolentRobot' 17 | ?

You must complete 2 of the 3 tasks listed below, and then wait 10 seconds before you can kill the Interviewer.

18 | : props.role.type === 'PatientRobot' 19 | ?

You must perform the penalty each time you violate your vulnerability.

20 | : undefined; 21 | 22 | return ( 23 | 24 | 25 |

Once they start the timer, the Interviewer will ask you a series of questions to try to determine whether you are a human or a robot.

26 | 27 | {robotPrompt} 28 | 29 | 30 | 31 |

Penalty: {props.penalty}

32 |

Suspect background: {props.suspectBackground}

33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/WaitPacketSelection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { Page } from './elements/Page'; 5 | import { P } from './elements/P'; 6 | import { Help } from './elements/Help'; 7 | 8 | interface IProps { 9 | position: InterviewPosition, 10 | } 11 | 12 | export const WaitPacketSelection: React.FunctionComponent = props => { 13 | return ( 14 | 15 | 16 |

Wait for the Interviewer to select an interview packet.

17 |
18 | ); 19 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/WaitPenalty.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InterviewPosition } from '../interviewReducer'; 3 | import { PositionHeader } from './elements/PositionHeader'; 4 | import { Page } from './elements/Page'; 5 | import { P } from './elements/P'; 6 | import { Help } from './elements/Help'; 7 | 8 | interface IProps { 9 | position: InterviewPosition; 10 | } 11 | 12 | export const WaitPenalty: React.FunctionComponent = props => { 13 | const msg = props.position === InterviewPosition.Interviewer 14 | ?

Wait for the Suspect to choose a penalty.

15 | :

Wait for the Interviewer to discard a penalty.

16 | 17 | return ( 18 | 19 | 20 | {msg} 21 | 22 | ); 23 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/WaitingForOpponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './GameLink.css'; 3 | import { Typography, Link } from '@material-ui/core'; 4 | import { Page } from './elements/Page'; 5 | import { P } from './elements/P'; 6 | 7 | interface IProps { 8 | interviewID: string; 9 | } 10 | 11 | export const WaitingForOpponent: React.FunctionComponent = props => { 12 | const fullLocation = document.location.toString(); 13 | const strippedProtocol = fullLocation.substr(fullLocation.indexOf('//') + 2); 14 | 15 | const fixedLocation = addWordBreaks( 16 | strippedProtocol.substr(0, strippedProtocol.indexOf(props.interviewID)), 17 | char => char === '/' || char === '.' 18 | ); 19 | const detailLocation = addWordBreaks( 20 | props.interviewID, 21 | char => char === char.toUpperCase() 22 | ); 23 | 24 | return ( 25 | 26 | Waiting for opponent to join 27 | 28 |

Invite a friend by giving them this link:

29 | 30 |

31 | {fixedLocation}{detailLocation} 32 |

33 | 34 |

35 | Don't open the link yourself, or you will become your own opponent. 36 |

37 |
38 | ); 39 | } 40 | 41 | function addWordBreaks(text: string, shouldBreak: (char: string) => boolean) { 42 | const results: JSX.Element[] = []; 43 | 44 | let word = ''; 45 | let i = 0; 46 | 47 | for (const char of text) { 48 | if (shouldBreak(char) && word.length > 0) { 49 | if (results.length > 0) { 50 | results.push( 51 | 52 | 53 | {word} 54 | 55 | ) 56 | } 57 | else { 58 | results.push( 59 | 60 | {word} 61 | 62 | ) 63 | } 64 | 65 | word = ''; 66 | } 67 | 68 | word = word + char; 69 | } 70 | 71 | if (word.length > 0) { 72 | results.push( 73 | 74 | 75 | {word} 76 | 77 | ); 78 | } 79 | 80 | return results; 81 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/WaitingQuestionDisplay.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IInterviewQuestion } from './elements/InterviewQuestion'; 3 | import { InterviewPosition } from '../interviewReducer'; 4 | import { PositionHeader } from './elements/PositionHeader'; 5 | import { SortableQuestions } from './elements/SortableQuestions'; 6 | import { Page } from './elements/Page'; 7 | import { P } from './elements/P'; 8 | import { Help } from './elements/Help'; 9 | 10 | interface IProps { 11 | questions: IInterviewQuestion[], 12 | sortQuestions: (questions: IInterviewQuestion[]) => void; 13 | } 14 | 15 | export const WaitingQuestionDisplay: React.FunctionComponent = props => { 16 | return ( 17 | 18 | 19 | 20 |

21 | Waiting for the Suspect to select their background. 22 |
Your questions are as follows: 23 |

24 | 25 | 29 |
30 | ); 31 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/ActionSet.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { makeStyles } from '@material-ui/core'; 3 | 4 | const useStyles = makeStyles(theme => ({ 5 | root: { 6 | display: 'flex', 7 | justifyContent: 'space-evenly', 8 | marginTop: '2em', 9 | }, 10 | })); 11 | 12 | export const ActionSet: React.FunctionComponent = props => { 13 | const classes = useStyles(); 14 | 15 | return ( 16 |
17 | {props.children} 18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/Choice.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { makeStyles, ButtonGroup } from '@material-ui/core'; 3 | 4 | const useStyles = makeStyles(theme => ({ 5 | root: { 6 | display: 'flex', 7 | flexDirection: 'column', 8 | alignItems: 'center', 9 | }, 10 | })); 11 | 12 | export const Choice: React.FunctionComponent = props => { 13 | const classes = useStyles(); 14 | 15 | return ( 16 |
17 | 18 | {props.children} 19 | 20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/ChoiceArray.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button } from '@material-ui/core'; 3 | import { Choice } from './Choice'; 4 | 5 | interface IProps { 6 | options: string[]; 7 | action?: (index: number) => void; 8 | } 9 | 10 | export const ChoiceArray: React.FunctionComponent = props => { 11 | const options = props.options.map((val: string, index: number) => { 12 | const onClick = props.action ? () => props.action!(index) : undefined; 13 | return 14 | }); 15 | 16 | return ( 17 | 18 | {options} 19 | 20 | ) 21 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/Countdown.css: -------------------------------------------------------------------------------- 1 | @keyframes blinker { 2 | 40% { 3 | color: transparent; 4 | } 5 | 60% { 6 | color: white; 7 | } 8 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/Countdown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useEffect, useState } from 'react'; 3 | import './Countdown.css'; 4 | import { Fab, makeStyles } from '@material-ui/core'; 5 | import HourglassEmpty from '@material-ui/icons/HourglassEmpty'; 6 | import HourglassFull from '@material-ui/icons/HourglassFull'; 7 | 8 | interface IProps { 9 | duration: number, 10 | onElapsed?: () => void; 11 | } 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | wrapper: { 15 | position: 'absolute', 16 | left: 'calc(100% - 6.5em)', 17 | top: '0.25em', 18 | }, 19 | main: { 20 | position: 'fixed', 21 | paddingRight: '1em', 22 | pointerEvents: 'none', 23 | }, 24 | expired: { 25 | animation: 'blinker 1s step-start infinite', 26 | }, 27 | icon: { 28 | paddingRight: '0.15em', 29 | }, 30 | })) 31 | 32 | export const Countdown: React.FunctionComponent = props => { 33 | const classes = useStyles(); 34 | 35 | const [timeRemaining, setTimeRemaining] = useState(props.duration); 36 | 37 | const onElapsed = props.onElapsed; 38 | 39 | useEffect( 40 | () => { 41 | let interval: NodeJS.Timeout | undefined = setInterval(() => { 42 | setTimeRemaining(val => { 43 | if (interval !== undefined && val <= 1) { 44 | clearInterval(interval); 45 | interval = undefined; 46 | 47 | if (onElapsed) { 48 | onElapsed(); 49 | } 50 | return 0; 51 | } 52 | return val - 1; 53 | }); 54 | }, 1000); 55 | 56 | return () => { 57 | if (interval !== undefined) { 58 | clearInterval(interval); 59 | } 60 | } 61 | }, 62 | [onElapsed] 63 | ); 64 | 65 | const elapsed = timeRemaining <= 0; 66 | 67 | const minutes = Math.floor(timeRemaining / 60); 68 | let seconds = (timeRemaining - minutes * 60).toString(); 69 | if (seconds.length < 2) { 70 | seconds = `0${seconds}`; 71 | } 72 | 73 | const color = elapsed ? 'secondary' : 'primary'; 74 | 75 | const mainClasses = elapsed 76 | ? `${classes.main} ${classes.expired}` 77 | : classes.main; 78 | 79 | const icon = elapsed 80 | ? 81 | : 82 | 83 | return ( 84 |
85 | 86 | {icon} 87 | {minutes}:{seconds} 88 | 89 |
90 | ); 91 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/Help.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography, Popover, makeStyles, Link } from '@material-ui/core'; 3 | 4 | type Entry = 'positions' 5 | | 'roles' 6 | | 'penalty' 7 | | 'packet' 8 | | 'inducer' 9 | | 'background' 10 | | 'questions' 11 | | 'timer'; 12 | 13 | interface Props { 14 | entry: Entry; 15 | } 16 | 17 | const useStyles = makeStyles(theme => ({ 18 | button: { 19 | textDecorationLine: 'underline', 20 | textDecorationStyle: 'double', 21 | textDecorationColor: 'green', 22 | cursor: 'help', 23 | '&:hover': { 24 | color: 'green', 25 | }, 26 | }, 27 | popup: { 28 | padding: theme.spacing(2), 29 | }, 30 | })); 31 | 32 | export const Help: React.FC = props => { 33 | const classes = useStyles(); 34 | 35 | const [anchorEl, setAnchorEl] = React.useState(); 36 | 37 | const handleClick = (event: React.MouseEvent) => { 38 | setAnchorEl(event.currentTarget); 39 | }; 40 | 41 | const handleClose = () => { 42 | setAnchorEl(undefined); 43 | }; 44 | 45 | const open = Boolean(anchorEl); 46 | 47 | const id = open ? 'help' : undefined; 48 | 49 | return <> 50 | 57 | {props.children} 58 | 59 | 73 | {getContent(props.entry, classes.popup)} 74 | 75 | 76 | } 77 | 78 | function getContent(entry: Entry, className: string): JSX.Element { 79 | switch (entry) { 80 | case 'positions': 81 | return 82 |

The Interviewer must try to determine whether the Suspect is a human or a robot.

83 | 84 |

The Suspect should try to convince the Interviewer that they are human, regardless of their true nature.

85 |
86 | 87 | case 'roles': 88 | return 89 |

A human Suspect has nothing to hide, and no restrictions on their behaviour.

90 |

A patient robot has a restriction, something they cannot mention.

91 |

A violent robot has an obsession, and must complete tasks to fulfil this obsession and allow them to kill the Interviewer.

92 |
93 | 94 | case 'penalty': 95 | return 96 |

The penalty is a suspicious action that robots may perform under stress during the interview.

97 | 98 |

Patient robots must perform the penalty once for each time they violate their restriction.

99 |

Violent robots may perform the penalty twice as part of their de-programming.

100 |

Human suspects should avoid performing the penalty, as this may make the Investigator think that they are a robot.

101 |
102 | 103 | case 'packet': 104 | return 105 |

An interview packet is a collection of question prompts and robot roles that relate to them.

106 |

It forms the topic of the interview.

107 |
108 | 109 | case 'inducer': 110 | return 111 |

The Interviewer asks the Suspect a question based on a simple diagram, and then administers the inducer. This reveals the Suspect's role to them.

112 |

Robots will see the same diagram as the Interviewer, but need time to read the details of their role.

113 |

Humans will need to solve more complicated diagram to answer the question.

114 |
115 | 116 | case 'background': 117 | return 118 |

Backgrounds provide the Suspect with a biographical detail to help them improvise a character.

119 |

The Investigator and the Suspect should act as if the background really is true.

120 |
121 | 122 | case 'questions': 123 | return 124 |

The questions in an interview packet relate directly to the patient and violent robot roles in that packet.

125 |

The Investigator can deviate as much as they like, but these questions should help draw out patterns of robot behavior.

126 |
127 | 128 | case 'timer': 129 | return 130 |

The Interviewer has 5 minutes to question the Suspect. Once the time has elapsed, they may ask one final question.

131 |

The Interviewer can conclude that the Suspect is a robot at any time, but must wait until the time has elapsed before they can conclude that the Suspect is a human.

132 |
133 | } 134 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/Interference.css: -------------------------------------------------------------------------------- 1 | .interference { 2 | display: flex; 3 | justify-content: center; 4 | white-space: pre; 5 | padding: 1em; 6 | font-family: 'Courier New', Courier, monospace; 7 | text-align: center; 8 | line-height: 1em; 9 | } 10 | 11 | .interference table { 12 | border-collapse: collapse; 13 | } 14 | 15 | .interference--solution { 16 | font-size: 1.25em; 17 | } 18 | 19 | .interference__cell { 20 | border: solid transparent 1px; 21 | width: 1.25em; 22 | height: 1.25em; 23 | text-align: center; 24 | vertical-align: middle; 25 | } 26 | 27 | .interference__cell--northBorder { 28 | border-top-color: black; 29 | } 30 | 31 | .interference__cell--southBorder { 32 | border-bottom-color: black; 33 | } 34 | 35 | .interference__cell--eastBorder { 36 | border-right-color: black; 37 | } 38 | 39 | .interference__cell--westBorder { 40 | border-left-color: black; 41 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/InterferencePattern.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './Interference.css'; 3 | import { Direction } from 'src/components/interviewReducer'; 4 | 5 | interface IProps { 6 | connections: Direction[][]; 7 | content: string[][]; 8 | } 9 | 10 | export const InterferencePattern: React.FunctionComponent = props => { 11 | const transposeContent = props.content[0].map((_, c) => props.content.map(row => row[c])); 12 | 13 | const rows = transposeContent.map((row, y) => { 14 | const cells = row.map((val, x) => { 15 | const classes = determineCellClasses(props.connections[x][y], x === 0, y === 0); 16 | return {val} 17 | }); 18 | 19 | return ( 20 | {cells} 21 | ) 22 | }); 23 | 24 | return ( 25 |
26 | 27 | 28 | {rows} 29 | 30 |
31 |
32 | ) 33 | } 34 | 35 | function determineCellClasses(connections: Direction, leftmost: boolean, topmost: boolean) { 36 | let classes = 'interference__cell'; 37 | 38 | if (topmost) { 39 | classes += ' interference__cell--northBorder'; 40 | } 41 | 42 | if (leftmost) { 43 | classes += ' interference__cell--westBorder'; 44 | } 45 | 46 | if ((connections & Direction.South) === 0) { 47 | classes += ' interference__cell--southBorder'; 48 | } 49 | if ((connections & Direction.East) === 0) { 50 | classes += ' interference__cell--eastBorder'; 51 | } 52 | 53 | return classes; 54 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/InterferenceSolution.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './Interference.css'; 3 | 4 | interface IProps { 5 | solution: string[]; 6 | } 7 | 8 | export const InterferenceSolution: React.FunctionComponent = props => { 9 | 10 | const splitAt = Math.ceil(props.solution.length / 2); 11 | 12 | const firstHalf = props.solution 13 | .slice(0, splitAt); 14 | 15 | const secondHalf = props.solution 16 | .slice(splitAt) 17 | .reverse(); 18 | 19 | if (secondHalf.length < firstHalf.length) { 20 | secondHalf.splice(1, 0, ''); 21 | } 22 | 23 | const firstRow: JSX.Element[] = []; 24 | const midRow: JSX.Element[] = []; 25 | const lastRow: JSX.Element[] = []; 26 | 27 | for (let i = 0; i < firstHalf.length; i++) { 28 | firstRow.push({firstHalf[i]}); 29 | lastRow.push({secondHalf[i]}); 30 | 31 | if (i < firstHalf.length - 1) { 32 | firstRow.push(→); 33 | lastRow.push(←); 34 | } 35 | 36 | if (i < firstHalf.length - 2) { 37 | midRow.push() 38 | midRow.push() 39 | } 40 | } 41 | 42 | return ( 43 |
44 | 45 | 46 | {firstRow} 47 | 48 | 49 | 52 | 53 | {lastRow} 54 | 55 |
50 | {midRow} 51 |
56 |
57 | ) 58 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/InterviewQuestion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button, Card, CardContent, Typography, makeStyles } from '@material-ui/core'; 3 | 4 | export interface IInterviewQuestion { 5 | challenge: string; 6 | examples: string[]; 7 | isPrimary: boolean; 8 | } 9 | 10 | interface IProps { 11 | question: IInterviewQuestion; 12 | sortUp?: () => void; 13 | sortDown?: () => void; 14 | } 15 | 16 | const useStyles = makeStyles(theme => ({ 17 | root: { 18 | position: 'relative', 19 | marginBottom: '1em', 20 | }, 21 | rootPrimary: { 22 | 23 | }, 24 | rootSecondary: { 25 | backgroundColor: '#eee', 26 | }, 27 | prefix: { 28 | textAlign: 'center', 29 | fontWeight: 'bold', 30 | textTransform: 'uppercase', 31 | }, 32 | suffix: { 33 | textAlign: 'center', 34 | fontWeight: 'bold', 35 | textTransform: 'uppercase', 36 | position: 'relative', 37 | top: '0.25em', 38 | }, 39 | secondary: { 40 | textAlign: 'center', 41 | fontStyle: 'italic', 42 | textTransform: 'uppercase', 43 | }, 44 | challenge: { 45 | marginTop: '0.25em', 46 | }, 47 | examples: { 48 | margin: '0.5em 0', 49 | fontStyle: 'italic', 50 | }, 51 | example: { 52 | 53 | }, 54 | sortUp: { 55 | position: 'absolute', 56 | right: '0', 57 | top: '0', 58 | }, 59 | sortDown: { 60 | position: 'absolute', 61 | bottom: '0', 62 | right: '0', 63 | } 64 | })); 65 | 66 | export const InterviewQuestion = React.forwardRef((props, ref) => { 67 | const classes = useStyles(); 68 | 69 | const rootClasses = props.question.isPrimary 70 | ? `${classes.root} ${classes.rootPrimary}` 71 | : `${classes.root} ${classes.rootSecondary}`; 72 | 73 | const examples = props.question.examples.map((q, i) => {q}); 74 | const secondary = props.question.isPrimary 75 | ? undefined 76 | : while fulfilling another prompt 77 | 78 | const sortUp = props.sortUp 79 | ? 84 | : undefined; 85 | 86 | const sortDown = props.sortDown 87 | ? 92 | : undefined; 93 | 94 | return ( 95 | 96 | 97 | Suspect must 98 | {secondary} 99 | 100 | {props.question.challenge}, e.g. 101 | 102 |
    103 | {examples} 104 |
105 | to be human 106 | {sortUp} 107 | {sortDown} 108 |
109 |
110 | 111 | ); 112 | }) 113 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/P.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | 4 | export const P: React.FunctionComponent = props => { 5 | return ( 6 | 7 | {props.children} 8 | 9 | ) 10 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/PacketDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ValueDisplay } from './ValueDisplay'; 3 | import { Help } from './Help'; 4 | 5 | interface Props { 6 | packet: string; 7 | } 8 | 9 | export const PacketDisplay: React.FC = props => ( 10 | Interview packet: 11 | ) -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container, makeStyles } from '@material-ui/core'; 3 | 4 | const useStyles = makeStyles(theme => ({ 5 | main: { 6 | position: 'relative', 7 | }, 8 | })); 9 | 10 | export const Page: React.FunctionComponent = props => { 11 | const classes = useStyles(); 12 | 13 | return ( 14 | 15 | {props.children} 16 | 17 | ) 18 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/PositionHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { InterviewPosition } from '../../interviewReducer'; 3 | import { Typography } from '@material-ui/core'; 4 | 5 | interface Props { 6 | position: InterviewPosition 7 | } 8 | 9 | export const PositionHeader: React.FC = props => { 10 | return {getText(props.position)} 11 | } 12 | 13 | function getText(position: InterviewPosition) { 14 | switch (position) { 15 | case InterviewPosition.Interviewer: 16 | return 'You are the interviewer'; 17 | case InterviewPosition.Suspect: 18 | return 'You are the suspect'; 19 | default: 20 | return 'You are a spectator'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/SortableQuestions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import FlipMove from 'react-flip-move'; 3 | import { IInterviewQuestion, InterviewQuestion } from './InterviewQuestion'; 4 | 5 | interface IProps { 6 | questions: IInterviewQuestion[]; 7 | sort: (questions: IInterviewQuestion[]) => void; 8 | } 9 | 10 | export const SortableQuestions: React.FunctionComponent = props => { 11 | const questions = props.questions.map((q, i) => { 12 | const sortUp = i === 0 13 | ? undefined 14 | : () => { 15 | const sorted = props.questions.slice(); 16 | sorted.splice(i, 1); 17 | const sortQuestion = props.questions[i]; 18 | sorted.splice(i - 1, 0, sortQuestion); 19 | props.sort(sorted); 20 | }; 21 | 22 | const sortDown = i === props.questions.length - 1 23 | ? undefined 24 | : () => { 25 | const sorted = props.questions.slice(); 26 | sorted.splice(i, 1); 27 | const sortQuestion = props.questions[i]; 28 | sorted.splice(i + 1, 0, sortQuestion); 29 | props.sort(sorted); 30 | }; 31 | 32 | return ( 33 | 39 | ) 40 | }); 41 | 42 | return ( 43 | 44 | {questions} 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/SuspectRole.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import { makeStyles, Card, Typography, CardContent } from '@material-ui/core'; 4 | 5 | export interface ISuspectRole { 6 | type: string; 7 | fault: string; 8 | traits: string, 9 | } 10 | 11 | interface IProps { 12 | role: ISuspectRole; 13 | } 14 | 15 | const useStyles = makeStyles(theme => ({ 16 | root: { 17 | margin: '1em 0', 18 | }, 19 | rootHuman: { 20 | 21 | }, 22 | rootPatient: { 23 | backgroundColor: '#eee', 24 | }, 25 | rootViolent: { 26 | backgroundColor: '#666', 27 | color: '#eee', 28 | }, 29 | title: { 30 | textAlign: 'center', 31 | }, 32 | traits: { 33 | display: 'flex', 34 | flexDirection: 'column', 35 | justifyContent: 'space-evenly', 36 | marginTop: '1em', 37 | '& ul': { 38 | margin: 0, 39 | }, 40 | '& p': { 41 | margin: 0, 42 | } 43 | } 44 | })) 45 | 46 | export const SuspectRole: React.FunctionComponent = props => { 47 | const classes = useStyles(); 48 | 49 | let displayName = props.role.type.replace('Robot', ' Robot'); 50 | if (props.role.type !== "Human") { 51 | displayName += ` (${props.role.fault})`; 52 | } 53 | 54 | return ( 55 | 56 | 57 | {displayName} 58 | 59 | 60 | 61 | ) 62 | } 63 | function getRootClasses(props: React.PropsWithChildren, classes: Record<"root" | "rootHuman" | "rootPatient" | "rootViolent" | "title" | "traits", string>) { 64 | let rootClasses: string; 65 | switch (props.role.type) { 66 | case 'Human': 67 | rootClasses = `${classes.root} ${classes.rootHuman}`; 68 | break; 69 | case 'PatientRobot': 70 | rootClasses = `${classes.root} ${classes.rootPatient}`; 71 | break; 72 | case 'ViolentRobot': 73 | rootClasses = `${classes.root} ${classes.rootViolent}`; 74 | break; 75 | default: 76 | rootClasses = classes.root; 77 | break; 78 | } 79 | return rootClasses; 80 | } 81 | 82 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewParts/elements/ValueDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography, makeStyles } from '@material-ui/core'; 3 | 4 | const useStyles = makeStyles(theme => ({ 5 | root: { 6 | textAlign: 'center', 7 | fontSize: '1.25em', 8 | }, 9 | })); 10 | 11 | interface IProps { 12 | value: string; 13 | } 14 | 15 | export const ValueDisplay: React.FunctionComponent = props => { 16 | const classes = useStyles(); 17 | 18 | return ( 19 | 20 | {props.children} 21 |
22 | {props.value} 23 |
24 |
25 | ) 26 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/components/interviewReducer.ts: -------------------------------------------------------------------------------- 1 | import { ISuspectRole } from './interviewParts/elements/SuspectRole'; 2 | import { IInterviewQuestion } from './interviewParts/elements/InterviewQuestion'; 3 | 4 | export enum InterviewStatus { 5 | NotConnected, 6 | Disconnected, 7 | InvalidSession, 8 | WaitingForOpponent, 9 | 10 | SelectingPositions, 11 | RoleConfirmed, 12 | 13 | PenaltySelection, 14 | PenaltyCalibration, 15 | 16 | PacketSelection, 17 | 18 | InducerPrompt, 19 | ShowingInducer, 20 | 21 | BackgroundSelection, 22 | 23 | ReadyToStart, 24 | InProgress, 25 | 26 | Finished, 27 | } 28 | 29 | export enum InterviewOutcome { 30 | Disconnected = 0, 31 | CorrectlyGuessedHuman, 32 | WronglyGuessedHuman, 33 | CorrectlyGuessedRobot, 34 | WronglyGuessedRobot, 35 | KilledInterviewer, 36 | } 37 | 38 | export enum InterviewPosition { 39 | Interviewer, 40 | Suspect, 41 | Spectator, 42 | None, 43 | } 44 | 45 | export enum Direction { 46 | None = 0, 47 | North = 1 << 0, 48 | South = 1 << 1, 49 | East = 1 << 2, 50 | West = 1 << 3, 51 | } 52 | 53 | export interface IPacket { 54 | name: string; 55 | difficulty: string; 56 | icon: string; 57 | } 58 | 59 | export interface IInterviewState { 60 | position: InterviewPosition; 61 | turn: InterviewPosition; 62 | status: InterviewStatus; 63 | outcome?: InterviewOutcome; 64 | choice: string[]; 65 | packet: string; 66 | packets?: IPacket[]; 67 | prompt: string; 68 | penalty: string; 69 | patternConnections?: Direction[][]; 70 | patternContent?: string[][]; 71 | patternSolution?: string[]; 72 | questions: IInterviewQuestion[]; 73 | suspectBackground: string; 74 | role?: ISuspectRole; 75 | duration: number; 76 | } 77 | 78 | export const initialState: IInterviewState = { 79 | choice: [], 80 | duration: 0, 81 | position: InterviewPosition.None, 82 | turn: InterviewPosition.Interviewer, 83 | packet: '', 84 | penalty: '', 85 | prompt: '', 86 | questions: [], 87 | status: InterviewStatus.NotConnected, 88 | suspectBackground: '', 89 | }; 90 | 91 | export type InterviewAction = { 92 | type: 'set position'; 93 | position: number; 94 | } | { 95 | type: 'swap position'; 96 | } | { 97 | type: 'set waiting for opponent'; 98 | } | { 99 | type: 'set selecting positions'; 100 | } | { 101 | type: 'set penalty choice'; 102 | options: string[] 103 | turn?: number 104 | } | { 105 | type: 'set penalty'; 106 | penalty: string; 107 | } | { 108 | type: 'set packet choice'; 109 | options: IPacket[]; 110 | } | { 111 | type: 'set packet'; 112 | packet: string; 113 | prompt: string; 114 | } | { 115 | type: 'prompt inducer'; 116 | solution: string[]; 117 | } | { 118 | type: 'spectator wait inducer'; 119 | role: ISuspectRole; 120 | solution: string[]; 121 | patternConnections?: number[][]; 122 | patternContent?: string[][]; 123 | } | { 124 | type: 'set role and pattern'; 125 | role: ISuspectRole; 126 | patternConnections: Direction[][]; 127 | patternContent: string[][]; 128 | } | { 129 | type: 'set role and solution'; 130 | role: ISuspectRole; 131 | solution: string[]; 132 | } | { 133 | type: 'show inducer'; 134 | } | { 135 | type: 'set questions'; 136 | questions: IInterviewQuestion[]; 137 | } | { 138 | type: 'set background choice'; 139 | options: string[]; 140 | } | { 141 | type: 'set background'; 142 | background: string; 143 | } | { 144 | type: 'start timer'; 145 | duration: number; 146 | } | { 147 | type: 'end game'; 148 | outcome: InterviewOutcome; 149 | role: ISuspectRole; 150 | } | { 151 | type: 'disconnect'; 152 | } | { 153 | type: 'invalid session'; 154 | } 155 | 156 | export function interviewReducer(state: IInterviewState, action: InterviewAction): IInterviewState { 157 | 158 | switch (action.type) { 159 | case 'set position': 160 | return { 161 | ...state, 162 | position: action.position, 163 | }; 164 | 165 | case 'swap position': 166 | return { 167 | ...state, 168 | position: [InterviewPosition.Suspect, InterviewPosition.Interviewer, InterviewPosition.Spectator][state.position], 169 | }; 170 | 171 | case 'set waiting for opponent': 172 | return { 173 | ...state, 174 | status: InterviewStatus.WaitingForOpponent, 175 | }; 176 | 177 | case 'set selecting positions': 178 | return { 179 | ...state, 180 | status: InterviewStatus.SelectingPositions, 181 | 182 | // clear any data from previous game 183 | choice: [], 184 | duration: 0, 185 | outcome: undefined, 186 | packet: '', 187 | packets: [], 188 | penalty: '', 189 | prompt: '', 190 | patternConnections: undefined, 191 | patternContent: undefined, 192 | patternSolution: undefined, 193 | questions: [], 194 | role: undefined, 195 | suspectBackground: '', 196 | }; 197 | 198 | case 'set penalty choice': 199 | return { 200 | ...state, 201 | status: InterviewStatus.PenaltySelection, 202 | choice: action.options, 203 | turn: action.turn === undefined ? InterviewPosition.Interviewer : action.turn, 204 | }; 205 | 206 | case 'set penalty': 207 | return { 208 | ...state, 209 | status: InterviewStatus.PenaltyCalibration, 210 | penalty: action.penalty, 211 | choice: [], 212 | }; 213 | 214 | case 'set packet choice': 215 | return { 216 | ...state, 217 | status: InterviewStatus.PacketSelection, 218 | packets: action.options, 219 | }; 220 | 221 | case 'set packet': 222 | return { 223 | ...state, 224 | packet: action.packet, 225 | prompt: action.prompt, 226 | choice: [], 227 | packets: [], 228 | }; 229 | 230 | case 'prompt inducer': 231 | return { 232 | ...state, 233 | status: InterviewStatus.InducerPrompt, 234 | patternSolution: action.solution.length > 0 ? action.solution : undefined, 235 | }; 236 | 237 | case 'spectator wait inducer': 238 | return { 239 | ...state, 240 | status: InterviewStatus.InducerPrompt, 241 | role: action.role, 242 | patternSolution: action.solution.length > 0 ? action.solution : undefined, 243 | patternConnections: action.patternConnections, 244 | patternContent: action.patternContent, 245 | }; 246 | 247 | case 'set role and pattern': 248 | return { 249 | ...state, 250 | role: action.role, 251 | patternConnections: action.patternConnections, 252 | patternContent: action.patternContent, 253 | }; 254 | 255 | case 'set role and solution': 256 | return { 257 | ...state, 258 | role: action.role, 259 | patternSolution: action.solution, 260 | }; 261 | 262 | case 'set questions': 263 | return { 264 | ...state, 265 | questions: action.questions, 266 | }; 267 | 268 | case 'show inducer': 269 | return { 270 | ...state, 271 | status: InterviewStatus.ShowingInducer, 272 | }; 273 | 274 | case 'set background choice': 275 | return { 276 | ...state, 277 | status: InterviewStatus.BackgroundSelection, 278 | choice: action.options, 279 | }; 280 | 281 | case 'set background': 282 | return { 283 | ...state, 284 | status: InterviewStatus.ReadyToStart, 285 | suspectBackground: action.background, 286 | }; 287 | 288 | case 'start timer': 289 | return { 290 | ...state, 291 | status: InterviewStatus.InProgress, 292 | duration: action.duration, 293 | }; 294 | 295 | case 'end game': 296 | return { 297 | ...state, 298 | status: InterviewStatus.Finished, 299 | outcome: action.outcome, 300 | role: action.role, 301 | }; 302 | 303 | case 'disconnect': 304 | return { 305 | ...state, 306 | status: InterviewStatus.Disconnected, 307 | }; 308 | 309 | case 'invalid session': 310 | return { 311 | ...state, 312 | status: InterviewStatus.InvalidSession, 313 | }; 314 | } 315 | } -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 4 | import { About } from './components/About'; 5 | import { Home } from './components/Home'; 6 | import { Host } from './components/Host'; 7 | import { Interview } from './components/Interview'; 8 | 9 | const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href')!; 10 | ReactDOM.render( 11 | ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ), 19 | document.getElementById('root') 20 | ); 21 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /RobotInterrogation/ClientApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "build/dist", 5 | "target": "es5", 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "esnext" 10 | ], 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /RobotInterrogation/Controllers/DataController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Options; 5 | using RobotInterrogation.Models; 6 | using RobotInterrogation.Services; 7 | 8 | namespace RobotInterrogation.Controllers 9 | { 10 | [ApiController] 11 | [Route("api/[controller]")] 12 | public class DataController : Controller 13 | { 14 | [HttpGet("[action]")] 15 | public string GetNextSessionID([FromServices] InterviewService service) 16 | { 17 | return service.GetNewInterviewID(); 18 | } 19 | 20 | [HttpGet("[action]")] 21 | public IEnumerable ListPackets([FromServices] IOptions configuration) 22 | { 23 | return configuration.Value.Packets; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RobotInterrogation/Hubs/InterviewHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | using Microsoft.Extensions.Options; 3 | using RobotInterrogation.Models; 4 | using RobotInterrogation.Services; 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | 10 | namespace RobotInterrogation.Hubs 11 | { 12 | public interface IInterviewMessages 13 | { 14 | Task SetPosition(int position); 15 | 16 | Task SetWaitingForPlayer(); 17 | Task SetPlayersPresent(); 18 | 19 | Task SwapPositions(); 20 | 21 | Task ShowPenaltyChoice(List options); 22 | Task WaitForPenaltyChoice(); 23 | Task SetPenalty(string penalty); 24 | 25 | Task ShowPacketChoice(PacketInfo[] options); 26 | Task WaitForPacketChoice(); 27 | Task SetPacket(string packetName, string packetPrompt); 28 | 29 | Task ShowInducerPrompt(List solution); 30 | Task WaitForInducer(); 31 | 32 | Task SetRoleWithPattern(SuspectRole role, int[][] connections, string[][] contents); 33 | Task SetRoleWithSolution(SuspectRole role, List solution); 34 | 35 | Task SetQuestions(IList questions); 36 | 37 | Task ShowInducer(); 38 | 39 | Task ShowSuspectBackgroundChoice(List notes); 40 | Task WaitForSuspectBackgroundChoice(); 41 | Task SetSuspectBackground(string note); 42 | 43 | Task StartTimer(int duration); 44 | 45 | Task EndGame(int endType, SuspectRole role); 46 | 47 | Task SpectatorWaitForPenaltyChoice(List options, int turn); 48 | Task SpectatorWaitForInducer(SuspectRole role, List solution); 49 | Task SpectatorWaitForInducer(SuspectRole role, List solution, int[][] connections, string[][] contents); 50 | Task SpectatorWaitForSuspectBackgroundChoice(List notes); 51 | } 52 | 53 | public class InterviewHub : Hub 54 | { 55 | public InterviewHub(InterviewService service, IOptions configuration) 56 | { 57 | Service = service; 58 | Configuration = configuration.Value; 59 | } 60 | 61 | private static ConcurrentDictionary UserSessions = new ConcurrentDictionary(); 62 | private string SessionID => UserSessions[Context.ConnectionId]; 63 | 64 | private InterviewService Service { get; } 65 | private GameConfiguration Configuration { get; } 66 | 67 | public override async Task OnDisconnectedAsync(Exception exception) 68 | { 69 | var player = Service.GetPlayerByConnectionID(Context.ConnectionId); 70 | if (player.Position == PlayerPosition.Spectator) 71 | { 72 | Service.RemovePlayer(player); 73 | } 74 | else 75 | { 76 | await Clients 77 | .GroupExcept(SessionID, Context.ConnectionId) 78 | .EndGame((int)InterviewOutcome.Disconnected, null); 79 | 80 | Service.RemoveInterview(SessionID); 81 | UserSessions.TryRemove(Context.ConnectionId, out _); 82 | await base.OnDisconnectedAsync(exception); 83 | } 84 | } 85 | 86 | private void EnsureIsInterviewer(Interview interview) 87 | { 88 | if (Context.ConnectionId != Service.GetInterviewerConnectionID(interview)) 89 | throw new Exception("Only the interviewer can issue this command for session " + SessionID); 90 | } 91 | 92 | private void EnsureIsSuspect(Interview interview) 93 | { 94 | if (Context.ConnectionId != Service.GetSuspectConnectionID(interview)) 95 | throw new Exception("Only the suspect can issue this command for session " + SessionID); 96 | } 97 | 98 | public async Task Join(string session) 99 | { 100 | if (!Service.TryGetInterview(session, out Interview interview)) 101 | return false; 102 | 103 | //if (interview.Status != InterviewStatus.WaitingForConnections) 104 | // return false; 105 | 106 | if (!Service.TryAddUser(interview, Context.ConnectionId)) 107 | return false; 108 | 109 | UserSessions[Context.ConnectionId] = session; 110 | await Groups.AddToGroupAsync(Context.ConnectionId, session); 111 | 112 | var player = Service.GetPlayerByConnectionID(Context.ConnectionId); 113 | 114 | await Clients.Caller.SetPosition((int)player.Position); 115 | 116 | if (player.Position == PlayerPosition.Interviewer) 117 | { 118 | await Clients 119 | .Group(session) 120 | .SetWaitingForPlayer(); 121 | } 122 | else if (player.Position == PlayerPosition.Suspect) 123 | { 124 | interview.Status = InterviewStatus.SelectingPositions; 125 | 126 | await Clients 127 | .Group(session) 128 | .SetPlayersPresent(); 129 | } else //Spectator 130 | { 131 | var client = Clients.Client(Context.ConnectionId); 132 | switch (interview.Status) 133 | { 134 | case InterviewStatus.WaitingForConnections: 135 | await client.SetWaitingForPlayer(); 136 | break; 137 | case InterviewStatus.SelectingPositions: 138 | await client.SetPlayersPresent(); 139 | break; 140 | case InterviewStatus.SelectingPenalty_Interviewer: 141 | await client.SpectatorWaitForPenaltyChoice(interview.Penalties, (int)PlayerPosition.Interviewer); 142 | break; 143 | case InterviewStatus.SelectingPenalty_Suspect: 144 | await client.SpectatorWaitForPenaltyChoice(interview.Penalties, (int)PlayerPosition.Suspect); 145 | break; 146 | case InterviewStatus.CalibratingPenalty: 147 | await client.SetPenalty(interview.Penalties[0]); 148 | break; 149 | case InterviewStatus.SelectingPacket: 150 | await client.WaitForPacketChoice(); 151 | break; 152 | case InterviewStatus.PromptingInducer: 153 | case InterviewStatus.SolvingInducer: 154 | if (interview.Role.Type == SuspectRoleType.Human) 155 | await client 156 | .SpectatorWaitForInducer( 157 | interview.Role, 158 | interview.InterferencePattern.SolutionSequence, 159 | interview.InterferencePattern.Connections.ToJaggedArray(val => (int)val), 160 | interview.InterferencePattern.CellContents.ToJaggedArray(val => val ?? string.Empty) 161 | ); 162 | else 163 | await client 164 | .SpectatorWaitForInducer( 165 | interview.Role, 166 | interview.InterferencePattern.SolutionSequence 167 | ); 168 | break; 169 | case InterviewStatus.SelectingSuspectBackground: 170 | await client.SpectatorWaitForSuspectBackgroundChoice(interview.SuspectBackgrounds); 171 | break; 172 | case InterviewStatus.ReadyToStart: 173 | await client.SetSuspectBackground(interview.SuspectBackgrounds[0]); 174 | break; 175 | case InterviewStatus.InProgress: 176 | await client.StartTimer(Configuration.Duration); 177 | break; 178 | case InterviewStatus.Finished: 179 | await client.EndGame((int)interview.Outcome, interview.Role); 180 | break; 181 | } 182 | 183 | } 184 | 185 | return true; 186 | } 187 | 188 | public async Task ConfirmPositions() 189 | { 190 | var interview = Service.GetInterviewWithStatus(SessionID, InterviewStatus.SelectingPositions); 191 | 192 | EnsureIsInterviewer(interview); 193 | 194 | interview.Status = InterviewStatus.SelectingPenalty_Interviewer; 195 | 196 | Service.AllocatePenalties(interview); 197 | 198 | await Clients 199 | .Client(Service.GetInterviewerConnectionID(interview)) 200 | .ShowPenaltyChoice(interview.Penalties); 201 | 202 | await Clients 203 | .Client(Service.GetSuspectConnectionID(interview)) 204 | .WaitForPenaltyChoice(); 205 | 206 | await Clients 207 | .GroupExcept(SessionID, Service.GetInterviewerConnectionID(interview), Service.GetSuspectConnectionID(interview)) 208 | .SpectatorWaitForPenaltyChoice(interview.Penalties, (int)PlayerPosition.Interviewer); 209 | } 210 | 211 | public async Task SwapPositions() 212 | { 213 | var interview = Service.GetInterviewWithStatus(SessionID, InterviewStatus.SelectingPositions); 214 | 215 | EnsureIsInterviewer(interview); 216 | 217 | var tmp = interview.InterviewerIndex; 218 | interview.InterviewerIndex = interview.SuspectIndex; 219 | interview.SuspectIndex = tmp; 220 | 221 | await Clients 222 | .Group(SessionID) 223 | .SwapPositions(); 224 | } 225 | 226 | public async Task Select(int index) 227 | { 228 | var interview = Service.GetInterview(SessionID); 229 | 230 | switch (interview.Status) 231 | { 232 | case InterviewStatus.SelectingPenalty_Interviewer: 233 | await DiscardSinglePenalty(index, interview); 234 | break; 235 | 236 | case InterviewStatus.SelectingPenalty_Suspect: 237 | await AllocatePenalty(index, interview); 238 | break; 239 | 240 | case InterviewStatus.CalibratingPenalty: 241 | EnsureIsInterviewer(interview); 242 | 243 | interview.Status = InterviewStatus.SelectingPacket; 244 | 245 | await ShowPacketChoice(interview); 246 | break; 247 | 248 | case InterviewStatus.SelectingPacket: 249 | EnsureIsInterviewer(interview); 250 | await SetPacket(interview, index); 251 | Service.AllocateRole(interview); 252 | await ShowInducerPrompt(interview); 253 | interview.Status = InterviewStatus.PromptingInducer; 254 | break; 255 | 256 | case InterviewStatus.PromptingInducer: 257 | await SetSuspectRole(interview); 258 | await ShowQuestions(interview); 259 | interview.Status = InterviewStatus.SolvingInducer; 260 | break; 261 | 262 | case InterviewStatus.SolvingInducer: 263 | EnsureIsInterviewer(interview); 264 | 265 | await ShowSuspectBackgrounds(interview, index == 0); 266 | interview.Status = InterviewStatus.SelectingSuspectBackground; 267 | break; 268 | 269 | case InterviewStatus.SelectingSuspectBackground: 270 | EnsureIsSuspect(interview); 271 | await SetSuspectBackground(interview, index); 272 | interview.Status = InterviewStatus.ReadyToStart; 273 | break; 274 | 275 | default: 276 | throw new Exception("Invalid command for current status of session " + SessionID); 277 | } 278 | } 279 | 280 | private async Task SetPacket(Interview interview, int index) 281 | { 282 | Service.SetPacketAndInducer(interview, index); 283 | 284 | await Clients.Group(SessionID) 285 | .SetPacket(interview.Packet.Name, interview.Packet.Prompt); 286 | } 287 | 288 | private async Task SetSuspectRole(Interview interview) 289 | { 290 | var suspectClient = Clients 291 | .Client(Service.GetSuspectConnectionID(interview)); 292 | 293 | var pattern = interview.InterferencePattern; 294 | 295 | if (interview.Role.Type == SuspectRoleType.Human) 296 | { 297 | await suspectClient 298 | .SetRoleWithPattern( 299 | interview.Role, 300 | pattern.Connections.ToJaggedArray(val => (int)val), 301 | pattern.CellContents.ToJaggedArray(val => val ?? string.Empty) 302 | ); 303 | 304 | await suspectClient.ShowInducer(); 305 | } 306 | else 307 | { 308 | await suspectClient 309 | .SetRoleWithSolution(interview.Role, pattern.SolutionSequence); 310 | 311 | await suspectClient.ShowInducer(); 312 | } 313 | } 314 | 315 | private async Task ShowQuestions(Interview interview) 316 | { 317 | var interviewer = Clients 318 | .Client(Service.GetInterviewerConnectionID(interview)); 319 | 320 | await interviewer 321 | .SetQuestions(interview.Packet.Questions); 322 | 323 | await interviewer.ShowInducer(); 324 | } 325 | 326 | private async Task DiscardSinglePenalty(int index, Interview interview) 327 | { 328 | interview.Status = InterviewStatus.SelectingPenalty_Suspect; 329 | 330 | if (index < 0 || index >= interview.Penalties.Count) 331 | throw new IndexOutOfRangeException($"Interviewer penalty selection must be between 0 and {interview.Penalties.Count}, but is {index}"); 332 | 333 | interview.Penalties.RemoveAt(index); 334 | 335 | await Clients 336 | .Client(Service.GetInterviewerConnectionID(interview)) 337 | .WaitForPenaltyChoice(); 338 | 339 | await Clients 340 | .Client(Service.GetSuspectConnectionID(interview)) 341 | .ShowPenaltyChoice(interview.Penalties); 342 | 343 | await Clients 344 | .GroupExcept(SessionID, Service.GetInterviewerConnectionID(interview), Service.GetSuspectConnectionID(interview)) 345 | .SpectatorWaitForPenaltyChoice(interview.Penalties, (int)PlayerPosition.Suspect); 346 | } 347 | 348 | private async Task AllocatePenalty(int index, Interview interview) 349 | { 350 | interview.Status = InterviewStatus.CalibratingPenalty; 351 | 352 | // the specified index is the one to keep 353 | int removeIndex = index == 0 ? 1 : 0; 354 | interview.Penalties.RemoveAt(removeIndex); 355 | 356 | await Clients 357 | .Group(SessionID) 358 | .SetPenalty(interview.Penalties[0]); 359 | } 360 | 361 | private async Task ShowPacketChoice(Interview interview) 362 | { 363 | var packets = Service.GetAllPackets(); 364 | 365 | await Clients 366 | .Client(Service.GetInterviewerConnectionID(interview)) 367 | .ShowPacketChoice(packets); 368 | 369 | await Clients 370 | .GroupExcept(SessionID, Service.GetInterviewerConnectionID(interview)) 371 | .WaitForPacketChoice(); 372 | } 373 | 374 | private async Task ShowInducerPrompt(Interview interview) 375 | { 376 | await Clients 377 | .Client(Service.GetInterviewerConnectionID(interview)) 378 | .ShowInducerPrompt(interview.InterferencePattern.SolutionSequence); 379 | 380 | await Clients 381 | .Client(Service.GetSuspectConnectionID(interview)) 382 | .WaitForInducer(); 383 | 384 | if (interview.Role.Type == SuspectRoleType.Human) 385 | await Clients 386 | .GroupExcept(SessionID, Service.GetInterviewerConnectionID(interview), Service.GetSuspectConnectionID(interview)) 387 | .SpectatorWaitForInducer( 388 | interview.Role, 389 | interview.InterferencePattern.SolutionSequence, 390 | interview.InterferencePattern.Connections.ToJaggedArray(val => (int)val), 391 | interview.InterferencePattern.CellContents.ToJaggedArray(val => val ?? string.Empty) 392 | ); 393 | else 394 | await Clients 395 | .GroupExcept(SessionID, Service.GetInterviewerConnectionID(interview), Service.GetSuspectConnectionID(interview)) 396 | .SpectatorWaitForInducer( 397 | interview.Role, 398 | interview.InterferencePattern.SolutionSequence 399 | ); 400 | } 401 | 402 | private async Task ShowSuspectBackgrounds(Interview interview, bool suspectCanChoose) 403 | { 404 | Service.AllocateSuspectBackgrounds(interview, suspectCanChoose ? 3 : 1); 405 | interview.Status = InterviewStatus.SelectingSuspectBackground; 406 | 407 | await Clients 408 | .Client(Service.GetInterviewerConnectionID(interview)) 409 | .WaitForSuspectBackgroundChoice(); 410 | 411 | await Clients 412 | .Client(Service.GetSuspectConnectionID(interview)) 413 | .ShowSuspectBackgroundChoice(interview.SuspectBackgrounds); 414 | 415 | await Clients 416 | .GroupExcept(SessionID, Service.GetInterviewerConnectionID(interview), Service.GetSuspectConnectionID(interview)) 417 | .SpectatorWaitForSuspectBackgroundChoice(interview.SuspectBackgrounds); 418 | } 419 | 420 | private async Task SetSuspectBackground(Interview interview, int index) 421 | { 422 | var background = interview.SuspectBackgrounds[index]; 423 | interview.SuspectBackgrounds.Clear(); 424 | interview.SuspectBackgrounds.Add(background); 425 | 426 | await Clients 427 | .Group(SessionID) 428 | .SetSuspectBackground(interview.SuspectBackgrounds[0]); 429 | } 430 | 431 | public async Task StartInterview() 432 | { 433 | var interview = Service.GetInterviewWithStatus(SessionID, InterviewStatus.ReadyToStart); 434 | EnsureIsInterviewer(interview); 435 | 436 | await Clients 437 | .Group(SessionID) 438 | .StartTimer(Configuration.Duration); 439 | 440 | interview.Status = InterviewStatus.InProgress; 441 | interview.Started = DateTime.Now; 442 | } 443 | 444 | public async Task ConcludeInterview(bool isRobot) 445 | { 446 | var interview = Service.GetInterviewWithStatus(SessionID, InterviewStatus.InProgress); 447 | EnsureIsInterviewer(interview); 448 | 449 | // Cannot record the suspect as human before the time has elapsed. 450 | if (!isRobot && !Service.HasTimeElapsed(interview)) 451 | return; 452 | 453 | var outcome = Service.GuessSuspectRole(interview, isRobot); 454 | 455 | await Clients 456 | .Group(SessionID) 457 | .EndGame((int)outcome, interview.Role); 458 | } 459 | 460 | public async Task KillInterviewer() 461 | { 462 | var interview = Service.GetInterviewWithStatus(SessionID, InterviewStatus.InProgress); 463 | EnsureIsSuspect(interview); 464 | 465 | Service.KillInterviewer(interview); 466 | 467 | await Clients 468 | .Group(SessionID) 469 | .EndGame((int)InterviewOutcome.KilledInterviewer, interview.Role); 470 | } 471 | 472 | public async Task NewInterview() 473 | { 474 | Service.ResetInterview(SessionID); 475 | 476 | await Clients 477 | .Group(SessionID) 478 | .SetPlayersPresent(); 479 | } 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /RobotInterrogation/Models/GameConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace RobotInterrogation.Models 2 | { 3 | public class GameConfiguration 4 | { 5 | public int MaxPlayers { get; set; } 6 | public int Duration { get; set; } 7 | public string[] Penalties { get; set; } 8 | public string[] SuspectBackgrounds { get; set; } 9 | public Packet[] Packets { get; set; } 10 | public SuspectRole HumanRole { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /RobotInterrogation/Models/IDWordList.cs: -------------------------------------------------------------------------------- 1 | namespace RobotInterrogation.Models 2 | { 3 | public class IDGeneration 4 | { 5 | public int WordCount { get; set; } 6 | public string[] Words { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /RobotInterrogation/Models/InterferencePattern.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace RobotInterrogation.Models 8 | { 9 | public class InterferencePattern 10 | { 11 | public InterferencePattern(int width, int height) 12 | { 13 | Width = width; 14 | Height = height; 15 | Connections = new Direction[width, height]; 16 | } 17 | 18 | public int Width { get; } 19 | 20 | public int Height { get; } 21 | 22 | public Direction[,] Connections { get; } 23 | 24 | [Flags] 25 | public enum Direction 26 | { 27 | None = 0, 28 | North = 1 << 0, 29 | South = 1 << 1, 30 | East = 1 << 2, 31 | West = 1 << 3, 32 | } 33 | 34 | public List Markers { get; } = new List(); 35 | 36 | public List MarkerSequence { get; } = new List(); 37 | 38 | public List SolutionSequence => MarkerSequence 39 | .Select(index => ((char)('A' + index)).ToString()) 40 | .ToList(); 41 | 42 | public List> Arrows = new List>(); 43 | 44 | public string[,] CellContents 45 | { 46 | get 47 | { 48 | var results = new string[Width, Height]; 49 | 50 | foreach (var arrow in Arrows) 51 | results[arrow.Item1.X, arrow.Item1.Y] = GetArrowForDirection(arrow.Item2).ToString(); 52 | 53 | for (int index = 0; index < Markers.Count; index++) 54 | { 55 | var marker = Markers[index]; 56 | results[marker.X, marker.Y] = GetSymbolForMarker(index).ToString(); 57 | } 58 | 59 | return results; 60 | } 61 | } 62 | 63 | private static char GetArrowForDirection(Direction dir) 64 | { 65 | switch (dir) 66 | { 67 | case Direction.North: 68 | return '↑'; 69 | case Direction.South: 70 | return '↓'; 71 | case Direction.East: 72 | return '→'; 73 | case Direction.West: 74 | return '←'; 75 | default: 76 | return ' '; 77 | } 78 | } 79 | 80 | private static char GetSymbolForMarker(int index) 81 | { 82 | return (char)('A' + index); 83 | } 84 | 85 | #region serialization 86 | public override string ToString() 87 | { 88 | var output = new StringBuilder(); 89 | 90 | for (int y = 0; y < Height; y++) 91 | { 92 | var rowTop = new StringBuilder(); 93 | var rowMid = new StringBuilder(); 94 | 95 | for (int x = 0; x < Width; x++) 96 | { 97 | var thisCell = Connections[x, y]; 98 | 99 | var prevRowPrevCell = y == 0 100 | ? x > 0 101 | ? Direction.East | Direction.West 102 | : Direction.East | Direction.South 103 | : x > 0 104 | ? Connections[x - 1, y - 1] 105 | : Direction.North | Direction.South; 106 | 107 | rowTop.Append(DetermineCornerCharacter(prevRowPrevCell, thisCell)); 108 | rowTop.Append(DetermineVerticalCharacter(thisCell)); 109 | 110 | rowMid.Append(DetermineHorizontalCharacter(thisCell)); 111 | rowMid.Append(DetermineContentCharacter(x, y)); 112 | } 113 | 114 | var aboveLastCell = y > 0 115 | ? Connections[Width - 1, y - 1] 116 | : Direction.East | Direction.West; 117 | 118 | rowTop.Append(DetermineCornerCharacter(aboveLastCell, Direction.North | Direction.South)); 119 | rowMid.Append(DetermineHorizontalCharacter(Direction.North | Direction.South)); 120 | 121 | output.AppendLine(rowTop.ToString()); 122 | output.AppendLine(rowMid.ToString()); 123 | } 124 | 125 | { 126 | var rowTop = new StringBuilder(); 127 | 128 | int y = Height - 1; 129 | for (int x = 0; x < Width; x++) 130 | { 131 | var prevRowPrevCell = x > 0 132 | ? Connections[x - 1, y] 133 | : Direction.North | Direction.South; 134 | 135 | var fakeRow = Direction.East | Direction.West; 136 | 137 | rowTop.Append(DetermineCornerCharacter(prevRowPrevCell, fakeRow)); 138 | rowTop.Append(DetermineVerticalCharacter(fakeRow)); 139 | } 140 | 141 | rowTop.Append(DetermineCornerCharacter(Direction.None, Direction.North | Direction.West)); 142 | 143 | output.AppendLine(rowTop.ToString()); 144 | } 145 | 146 | return output.ToString(); 147 | } 148 | 149 | private static char DetermineVerticalCharacter(Direction connections) 150 | { 151 | return connections.HasFlag(Direction.North) 152 | ? ' ' 153 | : '─'; 154 | } 155 | 156 | private static char DetermineHorizontalCharacter(Direction connections) 157 | { 158 | return connections.HasFlag(Direction.West) 159 | ? ' ' 160 | : '│'; 161 | } 162 | 163 | private static char DetermineCornerCharacter(Direction topLeft, Direction bottomRight) 164 | { 165 | if (topLeft.HasFlag(Direction.East)) 166 | if (topLeft.HasFlag(Direction.South)) 167 | { 168 | if (bottomRight.HasFlag(Direction.West)) 169 | { 170 | if (bottomRight.HasFlag(Direction.North)) 171 | return ' '; 172 | else 173 | return '─'; // right-only-dash? 174 | } 175 | else if (bottomRight.HasFlag(Direction.North)) 176 | return '│'; // down-only dash? 177 | else 178 | return '┌'; 179 | } 180 | else 181 | { 182 | if (bottomRight.HasFlag(Direction.West)) 183 | { 184 | /* 185 | if (bottomRight.HasFlag(Direction.North)) 186 | return '─'; // left-only dash? 187 | else 188 | */ 189 | return '─'; 190 | } 191 | else if (bottomRight.HasFlag(Direction.North)) 192 | return '┐'; 193 | else 194 | return '┬'; 195 | } 196 | else if (topLeft.HasFlag(Direction.South)) 197 | { 198 | if (bottomRight.HasFlag(Direction.West)) 199 | { 200 | if (bottomRight.HasFlag(Direction.North)) 201 | return '│'; // up-only dash? 202 | else 203 | return '└'; 204 | } 205 | else if (bottomRight.HasFlag(Direction.North)) 206 | return '│'; 207 | else 208 | return '├'; 209 | } 210 | else 211 | { 212 | if (bottomRight.HasFlag(Direction.West)) 213 | { 214 | if (bottomRight.HasFlag(Direction.North)) 215 | return '┘'; 216 | else 217 | return '┴'; 218 | } 219 | else if (bottomRight.HasFlag(Direction.North)) 220 | return '┤'; 221 | else 222 | return '┼'; 223 | } 224 | } 225 | private char DetermineContentCharacter(int x, int y) 226 | { 227 | var point = new Point(x, y); 228 | 229 | var index = Markers.IndexOf(point); 230 | if (index != -1) 231 | return GetSymbolForMarker(index); 232 | 233 | var matchingArrow = Arrows.FirstOrDefault(arr => arr.Item1 == point); 234 | 235 | if (matchingArrow != null) 236 | return GetArrowForDirection(matchingArrow.Item2); 237 | 238 | return ' '; 239 | } 240 | #endregion serialization 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /RobotInterrogation/Models/Interview.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RobotInterrogation.Models 5 | { 6 | public class Interview 7 | { 8 | public string InterviewID { get; set; } 9 | 10 | public InterviewStatus Status { get; set; } = InterviewStatus.WaitingForConnections; 11 | 12 | public InterviewOutcome? Outcome { get; set; } 13 | 14 | public DateTime? Started { get; set; } 15 | 16 | public List Players { get; } = new List(); 17 | 18 | public int InterviewerIndex { get; set; } = -1; 19 | 20 | public int SuspectIndex { get; set; } = -1; 21 | 22 | public List Penalties { get; } = new List(); 23 | 24 | public List SuspectBackgrounds { get; } = new List(); 25 | 26 | public Packet Packet { get; set; } 27 | 28 | public SuspectRole Role { get; set; } 29 | 30 | public string Prompt { get; set; } 31 | 32 | public InterferencePattern InterferencePattern { get; set; } 33 | 34 | public List PrimaryQuestions { get; } = new List(); 35 | 36 | public List SecondaryQuestions { get; } = new List(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /RobotInterrogation/Models/InterviewOutcome.cs: -------------------------------------------------------------------------------- 1 | namespace RobotInterrogation.Models 2 | { 3 | public enum InterviewOutcome 4 | { 5 | Disconnected = 0, 6 | CorrectlyGuessedHuman, 7 | WronglyGuessedHuman, 8 | CorrectlyGuessedRobot, 9 | WronglyGuessedRobot, 10 | KilledInterviewer, 11 | } 12 | } -------------------------------------------------------------------------------- /RobotInterrogation/Models/InterviewStatus.cs: -------------------------------------------------------------------------------- 1 | namespace RobotInterrogation.Models 2 | { 3 | public enum InterviewStatus 4 | { 5 | WaitingForConnections, 6 | SelectingPositions, 7 | 8 | SelectingPenalty_Interviewer, 9 | SelectingPenalty_Suspect, 10 | CalibratingPenalty, 11 | 12 | SelectingPacket, 13 | 14 | PromptingInducer, 15 | SolvingInducer, 16 | 17 | SelectingSuspectBackground, 18 | 19 | ReadyToStart, 20 | 21 | InProgress, 22 | 23 | Finished, 24 | } 25 | } -------------------------------------------------------------------------------- /RobotInterrogation/Models/Packet.cs: -------------------------------------------------------------------------------- 1 | namespace RobotInterrogation.Models 2 | { 3 | public class PacketInfo 4 | { 5 | public string Name { get; set; } 6 | 7 | public string Difficulty { get; set; } 8 | 9 | public string Icon { get; set; } 10 | } 11 | 12 | public class Packet : PacketInfo 13 | { 14 | public string Prompt { get; set; } 15 | 16 | public Question[] Questions { get; set; } 17 | 18 | public SuspectRole[] Roles { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /RobotInterrogation/Models/Player.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace RobotInterrogation.Models 7 | { 8 | public class Player 9 | { 10 | public string ConnectionID { get; set; } 11 | 12 | public string InterviewID { get; set; } 13 | 14 | public string Name { get; set; } 15 | 16 | public PlayerPosition Position { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /RobotInterrogation/Models/PlayerPosition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace RobotInterrogation.Models 7 | { 8 | public enum PlayerPosition 9 | { 10 | Interviewer, 11 | Suspect, 12 | Spectator 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RobotInterrogation/Models/Question.cs: -------------------------------------------------------------------------------- 1 | namespace RobotInterrogation.Models 2 | { 3 | public class Question 4 | { 5 | public bool IsPrimary { get; set; } 6 | public string Challenge { get; set; } 7 | public string[] Examples { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /RobotInterrogation/Models/SuspectRole.cs: -------------------------------------------------------------------------------- 1 | namespace RobotInterrogation.Models 2 | { 3 | public class SuspectRole 4 | { 5 | public SuspectRoleType Type { get; set; } 6 | public string Fault { get; set; } 7 | public string Traits { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /RobotInterrogation/Models/SuspectRoleType.cs: -------------------------------------------------------------------------------- 1 | namespace RobotInterrogation.Models 2 | { 3 | public enum SuspectRoleType 4 | { 5 | Human = 0, 6 | PatientRobot, 7 | ViolentRobot, 8 | } 9 | } -------------------------------------------------------------------------------- /RobotInterrogation/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 |

21 |

22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 |

27 | -------------------------------------------------------------------------------- /RobotInterrogation/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace RobotInterrogation.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | public class ErrorModel : PageModel 14 | { 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public string RequestId { get; set; } 23 | 24 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 25 | 26 | public void OnGet() 27 | { 28 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RobotInterrogation/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using RobotInterrogation 2 | @namespace RobotInterrogation.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /RobotInterrogation/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | using RobotInterrogation.Services; 10 | 11 | namespace RobotInterrogation 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IHostBuilder CreateHostBuilder(string[] args) => 21 | Host.CreateDefaultBuilder(args) 22 | .ConfigureLogging(logging => 23 | logging.AddFilter(InterviewService.LogName, LogLevel.Information) 24 | ) 25 | .ConfigureWebHostDefaults(webBuilder => 26 | { 27 | webBuilder.UseStartup(); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /RobotInterrogation/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:58663/", 7 | "sslPort": 44376 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "RobotInterrogation": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /RobotInterrogation/RobotInterrogation.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | true 6 | Latest 7 | AspNetCoreModuleV2 8 | true 9 | false 10 | ClientApp\ 11 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 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 | %(DistFiles.Identity) 45 | PreserveNewest 46 | true 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /RobotInterrogation/Services/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RobotInterrogation.Services 4 | { 5 | public static class ExtensionMethods 6 | { 7 | public static T[][] ToJaggedArray(this T[,] twoDimensionalArray) 8 | { 9 | return ToJaggedArray(twoDimensionalArray, val => val); 10 | } 11 | 12 | public static TResult[][] ToJaggedArray(this TSource[,] twoDimensionalArray, Func convertValue) 13 | { 14 | int rowsFirstIndex = twoDimensionalArray.GetLowerBound(0); 15 | int rowsLastIndex = twoDimensionalArray.GetUpperBound(0); 16 | int numberOfRows = rowsLastIndex + 1; 17 | 18 | int columnsFirstIndex = twoDimensionalArray.GetLowerBound(1); 19 | int columnsLastIndex = twoDimensionalArray.GetUpperBound(1); 20 | int numberOfColumns = columnsLastIndex + 1; 21 | 22 | TResult[][] jaggedArray = new TResult[numberOfRows][]; 23 | for (int i = rowsFirstIndex; i <= rowsLastIndex; i++) 24 | { 25 | jaggedArray[i] = new TResult[numberOfColumns]; 26 | 27 | for (int j = columnsFirstIndex; j <= columnsLastIndex; j++) 28 | { 29 | jaggedArray[i][j] = convertValue(twoDimensionalArray[i, j]); 30 | } 31 | } 32 | return jaggedArray; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /RobotInterrogation/Services/InterferenceService.cs: -------------------------------------------------------------------------------- 1 | using RobotInterrogation.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.Linq; 6 | 7 | namespace RobotInterrogation.Services 8 | { 9 | public class InterferenceService 10 | { 11 | public InterferencePattern Generate(Random random, int simpleWidth = 7, int simpleHeight = 4, int numMarkers = 6) 12 | { 13 | // Generate a simple maze pattern. 14 | var simplePattern = GenerateSimple(random, simpleWidth, simpleHeight); 15 | 16 | // Convert each cell of the maze into 2x2 cell squares, and add lines down the middle of each squares. 17 | var pattern = DoubleUp(simplePattern); 18 | 19 | // Place more than the required number of markers, initially. 20 | List markerPositions = PlaceMarkers(random, pattern.Width, pattern.Height, numMarkers * 4); 21 | 22 | // Determine the path through the maze. 23 | var markerData = SolveSequence(random, markerPositions, pattern.Connections); 24 | 25 | // Remove markers from this path that are too close together, leaving us with numMarkers. 26 | RemoveClosestMarkers(markerData, numMarkers); 27 | 28 | // Save the marker positions, in "display" order. 29 | pattern.Markers.AddRange 30 | ( 31 | markerData 32 | .OrderBy(step => step.SortOrder) 33 | .Select(step => step.Cell) 34 | ); 35 | 36 | // Now save the solution. 37 | pattern.MarkerSequence.AddRange 38 | ( 39 | markerData.Select(step => step.SortOrder) 40 | ); 41 | 42 | // For each marker, decide which arrow to keep, and add it to the pattern. 43 | foreach (var marker in markerData) 44 | { 45 | var subsequentCell = marker.SubsequentCell; 46 | var connections = pattern.Connections[subsequentCell.X, subsequentCell.Y]; 47 | 48 | // If there's space to continue in the subsequent direction, use that cell for the arrow. 49 | // Otherwise use the previous cell, which points at the marker cell. 50 | if (connections.HasFlag(marker.SubsequentCellDirection)) 51 | pattern.Arrows.Add(new Tuple(subsequentCell, marker.SubsequentCellDirection)); 52 | else 53 | pattern.Arrows.Add(new Tuple(marker.PreviousCell, marker.PreviousCellDirection)); 54 | } 55 | 56 | return pattern; 57 | } 58 | 59 | private struct Coord 60 | { 61 | public Coord(int x, int y, InterferencePattern.Direction direction, InterferencePattern.Direction opposite) 62 | { 63 | Direction = direction; 64 | OppositeDirection = opposite; 65 | X = x; 66 | Y = y; 67 | } 68 | 69 | public InterferencePattern.Direction Direction { get; } 70 | 71 | public InterferencePattern.Direction OppositeDirection { get; } 72 | 73 | public int X { get; } 74 | 75 | public int Y { get; } 76 | } 77 | 78 | private List CardinalOffsets { get; } = new List 79 | { 80 | new Coord(1, 0, InterferencePattern.Direction.East, InterferencePattern.Direction.West), 81 | new Coord(-1, 0, InterferencePattern.Direction.West, InterferencePattern.Direction.East), 82 | new Coord(0, 1, InterferencePattern.Direction.South, InterferencePattern.Direction.North), 83 | new Coord(0, -1, InterferencePattern.Direction.North, InterferencePattern.Direction.South), 84 | }; 85 | 86 | private InterferencePattern GenerateSimple(Random random, int width, int height) 87 | { 88 | var pattern = new InterferencePattern(width, height); 89 | GeneratePassagesFrom(0, 0, pattern, random); 90 | return pattern; 91 | } 92 | 93 | private void GeneratePassagesFrom(int startX, int startY, InterferencePattern pattern, Random random) 94 | { 95 | var directions = new List(CardinalOffsets); 96 | Shuffle(directions, random); 97 | 98 | foreach (var direction in directions) 99 | { 100 | int nextX = startX + direction.X; 101 | if (nextX < 0 || nextX >= pattern.Width) 102 | continue; 103 | 104 | int nextY = startY + direction.Y; 105 | if (nextY < 0 || nextY >= pattern.Height) 106 | continue; 107 | 108 | if (pattern.Connections[nextX, nextY] != InterferencePattern.Direction.None) 109 | continue; 110 | 111 | pattern.Connections[startX, startY] |= direction.Direction; 112 | pattern.Connections[nextX, nextY] |= direction.OppositeDirection; 113 | 114 | GeneratePassagesFrom(nextX, nextY, pattern, random); 115 | } 116 | } 117 | 118 | private InterferencePattern DoubleUp(InterferencePattern source) 119 | { 120 | var result = new InterferencePattern(source.Width * 2, source.Height * 2); 121 | 122 | for (int sourceX = 0; sourceX < source.Width; sourceX++) 123 | for (int sourceY = 0; sourceY < source.Height; sourceY++) 124 | { 125 | var destX = sourceX * 2; 126 | var destY = sourceY * 2; 127 | 128 | // Expand each source cell into 2x2 cells in the destination, filling the interior walls wherever there's an external connection. 129 | var sourceConnections = source.Connections[sourceX, sourceY]; 130 | 131 | bool midNorthOpen = !sourceConnections.HasFlag(InterferencePattern.Direction.North); 132 | bool midSouthOpen = !sourceConnections.HasFlag(InterferencePattern.Direction.South); 133 | bool midEastOpen = !sourceConnections.HasFlag(InterferencePattern.Direction.East); 134 | bool midWestOpen = !sourceConnections.HasFlag(InterferencePattern.Direction.West); 135 | 136 | var northWest = sourceConnections & (InterferencePattern.Direction.North | InterferencePattern.Direction.West); 137 | if (midNorthOpen) 138 | northWest |= InterferencePattern.Direction.East; 139 | if (midWestOpen) 140 | northWest |= InterferencePattern.Direction.South; 141 | result.Connections[destX, destY] = northWest; 142 | 143 | var northEast = sourceConnections & (InterferencePattern.Direction.North | InterferencePattern.Direction.East); 144 | if (midNorthOpen) 145 | northEast |= InterferencePattern.Direction.West; 146 | if (midEastOpen) 147 | northEast |= InterferencePattern.Direction.South; 148 | result.Connections[destX + 1, destY] = northEast; 149 | 150 | var southWest = sourceConnections & (InterferencePattern.Direction.South | InterferencePattern.Direction.West); 151 | if (midSouthOpen) 152 | southWest |= InterferencePattern.Direction.East; 153 | if (midWestOpen) 154 | southWest |= InterferencePattern.Direction.North; 155 | result.Connections[destX, destY + 1] = southWest; 156 | 157 | var southEast = sourceConnections & (InterferencePattern.Direction.South | InterferencePattern.Direction.East); 158 | if (midSouthOpen) 159 | southEast |= InterferencePattern.Direction.West; 160 | if (midEastOpen) 161 | southEast |= InterferencePattern.Direction.North; 162 | result.Connections[destX + 1, destY + 1] = southEast; 163 | } 164 | 165 | return result; 166 | } 167 | 168 | private void Shuffle(IList list, Random random) 169 | { 170 | int n = list.Count; 171 | while (n > 1) 172 | { 173 | n--; 174 | int k = random.Next(n + 1); 175 | T value = list[k]; 176 | list[k] = list[n]; 177 | list[n] = value; 178 | } 179 | } 180 | 181 | private List PlaceMarkers(Random random, int width, int height, int numMarkers) 182 | { 183 | var results = new List(); 184 | 185 | for (int i = 0; i < numMarkers; i++) 186 | { 187 | Point point; 188 | 189 | do 190 | { 191 | point = new Point(random.Next(width), random.Next(height)); 192 | } while (results.Contains(point)); 193 | 194 | results.Add(point); 195 | } 196 | 197 | return results; 198 | } 199 | 200 | private class SequenceMarker 201 | { 202 | public int SortOrder { get; set; } 203 | 204 | public Point Cell { get; set; } 205 | public int StepsToNextMarker { get; set; } 206 | 207 | public Point PreviousCell { get; set; } 208 | public Point SubsequentCell { get; set; } 209 | 210 | public InterferencePattern.Direction PreviousCellDirection { get; set; } 211 | public InterferencePattern.Direction SubsequentCellDirection { get; set; } 212 | } 213 | 214 | private List SolveSequence(Random random, List markers, InterferencePattern.Direction[,] connections) 215 | { 216 | var directions = new List(CardinalOffsets); 217 | Shuffle(directions, random); 218 | 219 | var results = new List(); 220 | var startPosition = markers.First(); 221 | var currentPosition = startPosition; 222 | var prevPosition = startPosition; 223 | var prevDirection = InterferencePattern.Direction.None; 224 | 225 | var stepsToNextMarker = 0; 226 | 227 | SequenceMarker prevMarker = new SequenceMarker 228 | { 229 | SortOrder = 0, 230 | Cell = currentPosition, 231 | }; 232 | 233 | do 234 | { 235 | stepsToNextMarker++; 236 | 237 | // Find a direction we can move in that isn't back to where we came from. 238 | var currentConnections = connections[currentPosition.X, currentPosition.Y]; 239 | var direction = directions.First 240 | ( 241 | direction => currentConnections.HasFlag(direction.Direction) 242 | && direction.OppositeDirection != prevDirection 243 | ); 244 | 245 | prevPosition = currentPosition; 246 | currentPosition.Offset(direction.X, direction.Y); 247 | prevDirection = direction.Direction; 248 | 249 | if (prevMarker.SubsequentCellDirection == InterferencePattern.Direction.None) 250 | { 251 | prevMarker.SubsequentCell = currentPosition; 252 | prevMarker.SubsequentCellDirection = prevDirection; 253 | } 254 | 255 | var markerId = markers.IndexOf(currentPosition); 256 | if (markerId != -1) 257 | { 258 | var test = markers[markerId]; 259 | 260 | // We've reached a new marker, so add it to the results. 261 | prevMarker = new SequenceMarker 262 | { 263 | SortOrder = markerId, 264 | Cell = currentPosition, 265 | PreviousCell = prevPosition, 266 | PreviousCellDirection = prevDirection, 267 | }; 268 | 269 | results.Add(prevMarker); 270 | 271 | // Update the previous marker with its # steps. 272 | prevMarker.StepsToNextMarker = stepsToNextMarker; 273 | stepsToNextMarker = 0; 274 | } 275 | } while (currentPosition != startPosition); 276 | 277 | var firstStep = results.First(); 278 | firstStep.PreviousCell = prevPosition; 279 | firstStep.PreviousCellDirection = prevDirection; 280 | 281 | return results; 282 | } 283 | 284 | private void RemoveClosestMarkers(List markerData, int targetNumMarkers) 285 | { 286 | // Remove the marker with the shortest StepsToNextMarker, until we have targetNumMarkers. 287 | while (markerData.Count > targetNumMarkers) 288 | { 289 | int removeIndex = FindMinIndex(markerData, out int removeLength); 290 | 291 | int prevIndex = removeIndex == 0 292 | ? markerData.Count - 1 293 | : removeIndex - 1; 294 | 295 | // The removed item's "steps to next" gets added onto the previous step. 296 | markerData[prevIndex].StepsToNextMarker += removeLength; 297 | 298 | var removeMarker = markerData[removeIndex]; 299 | markerData.RemoveAt(removeIndex); 300 | 301 | foreach (var step in markerData.Where(step => step.SortOrder > removeMarker.SortOrder)) 302 | step.SortOrder--; 303 | } 304 | } 305 | 306 | private int FindMinIndex(List values, out int minValue) 307 | { 308 | int index = 0; 309 | minValue = int.MaxValue; 310 | 311 | for (int i = 0; i < values.Count; i++) 312 | { 313 | var value = values[i].StepsToNextMarker; 314 | if (value < minValue) 315 | { 316 | index = i; 317 | minValue = value; 318 | } 319 | } 320 | 321 | return index; 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /RobotInterrogation/Services/InterviewService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Options; 3 | using RobotInterrogation.Models; 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text.Json; 9 | using System.Text.Json.Serialization; 10 | 11 | namespace RobotInterrogation.Services 12 | { 13 | public class InterviewService 14 | { 15 | private static ConcurrentDictionary Players = new ConcurrentDictionary(); 16 | private static ConcurrentDictionary Interviews = new ConcurrentDictionary(); 17 | 18 | private static Random IdGenerator = new Random(); 19 | 20 | private GameConfiguration Configuration { get; } 21 | private IDGeneration IDs { get; } 22 | private InterferenceService InterferenceService { get; } 23 | private ILogger Logger { get; } 24 | 25 | public const string LogName = "Interviews"; 26 | 27 | public InterviewService(IOptions configuration, IOptions idWords, InterferenceService interferenceService, ILoggerFactory logger) 28 | { 29 | Configuration = configuration.Value; 30 | IDs = idWords.Value; 31 | InterferenceService = interferenceService; 32 | Logger = logger.CreateLogger(LogName); 33 | } 34 | 35 | public string GetNewInterviewID() 36 | { 37 | lock (IdGenerator) 38 | { 39 | var interview = new Interview(); 40 | interview.InterviewID = GenerateID(); 41 | Interviews[interview.InterviewID.ToLower()] = interview; 42 | return interview.InterviewID; 43 | } 44 | } 45 | 46 | private string GenerateID() 47 | { 48 | if (IDs.WordCount <= 0) 49 | return string.Empty; 50 | 51 | string id; 52 | do 53 | { 54 | int[] iWords = new int[IDs.WordCount]; 55 | 56 | for (int i = 0; i < IDs.WordCount; i++) 57 | { 58 | do 59 | { 60 | int iWord = IdGenerator.Next(IDs.Words.Length); 61 | 62 | bool reused = iWords 63 | .Take(i) 64 | .Any(jWord => jWord == iWord); 65 | 66 | if (reused) 67 | continue; 68 | 69 | iWords[i] = iWord; 70 | break; 71 | } while (true); 72 | } 73 | 74 | id = string.Join("", iWords.Select(i => IDs.Words[i])); 75 | } while (Interviews.ContainsKey(id.ToLower())); 76 | 77 | return id; 78 | } 79 | 80 | public string GetInterviewerConnectionID(Interview interview) 81 | { 82 | return interview.Players[interview.InterviewerIndex].ConnectionID; 83 | } 84 | 85 | public string GetSuspectConnectionID(Interview interview) 86 | { 87 | return interview.Players[interview.SuspectIndex].ConnectionID; 88 | } 89 | 90 | public Player GetPlayerByConnectionID(string connectionID) 91 | { 92 | return Players[connectionID]; 93 | } 94 | 95 | public bool TryAddUser(Interview interview, string connectionID) 96 | { 97 | if (interview.Players.Count >= Configuration.MaxPlayers) 98 | return false; 99 | 100 | var player = new Player(); 101 | player.ConnectionID = connectionID; 102 | Players[connectionID] = player; 103 | interview.Players.Add(player); 104 | 105 | if (interview.InterviewerIndex == -1) 106 | { 107 | player.Position = PlayerPosition.Interviewer; 108 | interview.InterviewerIndex = interview.Players.Count - 1; 109 | } else if (interview.SuspectIndex == -1) 110 | { 111 | player.Position = PlayerPosition.Suspect; 112 | interview.SuspectIndex = interview.Players.Count - 1; 113 | } else 114 | { 115 | player.Position = PlayerPosition.Spectator; 116 | } 117 | 118 | player.InterviewID = interview.InterviewID; 119 | 120 | return true; 121 | } 122 | 123 | public void RemovePlayer(Player player) 124 | { 125 | GetInterview(player.InterviewID).Players.Remove(player); 126 | Players.Remove(player.ConnectionID, out Player p); 127 | } 128 | 129 | public void RemoveInterview(string interviewID) 130 | { 131 | Interview interview; 132 | if (!Interviews.TryRemove(interviewID.ToLower(), out interview)) 133 | { 134 | foreach(Player player in interview.Players) 135 | Players.TryRemove(player.ConnectionID, out Player p); 136 | return; 137 | } 138 | 139 | if (interview.Status == InterviewStatus.InProgress) 140 | { 141 | LogInterview(interview); 142 | } 143 | } 144 | 145 | public bool TryGetInterview(string interviewID, out Interview interview) 146 | { 147 | return Interviews.TryGetValue(interviewID.ToLower(), out interview); 148 | } 149 | 150 | public Interview GetInterview(string interviewID) 151 | { 152 | if (!Interviews.TryGetValue(interviewID.ToLower(), out Interview interview)) 153 | throw new Exception($"Invalid interview ID: {interviewID}"); 154 | 155 | return interview; 156 | } 157 | 158 | public Interview GetInterviewWithStatus(string interviewID, InterviewStatus status) 159 | { 160 | var interview = GetInterview(interviewID); 161 | 162 | if (interview.Status != status) 163 | throw new Exception($"Interview doesn't have the required status {status} - it is actually {interview.Status}"); 164 | 165 | return interview; 166 | } 167 | 168 | public bool HasTimeElapsed(Interview interview) 169 | { 170 | if (!interview.Started.HasValue) 171 | return false; 172 | 173 | return interview.Started.Value + TimeSpan.FromSeconds(Configuration.Duration) 174 | <= DateTime.Now; 175 | } 176 | 177 | private void AllocateRandomValues(IList source, IList destination, int targetNum) 178 | { 179 | destination.Clear(); 180 | 181 | var random = new Random(); 182 | 183 | while (destination.Count < targetNum) 184 | { 185 | int iSelection = random.Next(source.Count); 186 | T selection = source[iSelection]; 187 | 188 | if (destination.Contains(selection)) 189 | continue; 190 | 191 | destination.Add(selection); 192 | } 193 | } 194 | 195 | public void AllocatePenalties(Interview interview) 196 | { 197 | AllocateRandomValues(Configuration.Penalties, interview.Penalties, 3); 198 | } 199 | 200 | public PacketInfo[] GetAllPackets() 201 | { 202 | return Configuration.Packets; 203 | } 204 | 205 | public Packet GetPacket(int index) 206 | { 207 | return Configuration.Packets[index]; 208 | } 209 | 210 | public void AllocateRole(Interview interview) 211 | { 212 | var possibleRoles = new List(interview.Packet.Roles); 213 | 214 | // Always have a 50% chance of being human. 215 | possibleRoles.AddRange( 216 | interview.Packet.Roles.Select(role => Configuration.HumanRole) 217 | ); 218 | 219 | var roles = new List(); 220 | 221 | AllocateRandomValues(possibleRoles, roles, 1); 222 | 223 | interview.Role = roles.First(); 224 | } 225 | 226 | public void SetPacketAndInducer(Interview interview, int packetIndex) 227 | { 228 | interview.Packet = GetPacket(packetIndex); 229 | interview.InterferencePattern = InterferenceService.Generate(new Random()); 230 | } 231 | 232 | public void AllocateSuspectBackgrounds(Interview interview, int numOptions) 233 | { 234 | AllocateRandomValues(Configuration.SuspectBackgrounds, interview.SuspectBackgrounds, numOptions); 235 | } 236 | 237 | public InterviewOutcome GuessSuspectRole(Interview interview, bool guessIsRobot) 238 | { 239 | var actualRole = interview.Role.Type; 240 | 241 | InterviewOutcome outcome; 242 | 243 | if (guessIsRobot) 244 | { 245 | outcome = actualRole == SuspectRoleType.Human 246 | ? InterviewOutcome.WronglyGuessedRobot 247 | : InterviewOutcome.CorrectlyGuessedRobot; 248 | } 249 | else 250 | { 251 | outcome = actualRole == SuspectRoleType.Human 252 | ? InterviewOutcome.CorrectlyGuessedHuman 253 | : InterviewOutcome.WronglyGuessedHuman; 254 | } 255 | 256 | interview.Status = InterviewStatus.Finished; 257 | interview.Outcome = outcome; 258 | 259 | LogInterview(interview); 260 | 261 | return outcome; 262 | } 263 | 264 | public void KillInterviewer(Interview interview) 265 | { 266 | if (interview.Role.Type != SuspectRoleType.ViolentRobot) 267 | { 268 | throw new Exception("Suspect is not a violent robot, so cannot kill interviewer"); 269 | } 270 | 271 | interview.Status = InterviewStatus.Finished; 272 | interview.Outcome = InterviewOutcome.KilledInterviewer; 273 | 274 | LogInterview(interview); 275 | } 276 | 277 | public Interview ResetInterview(string interviewID) 278 | { 279 | var oldInterview = GetInterviewWithStatus(interviewID, InterviewStatus.Finished); 280 | 281 | var newInterview = new Interview(); 282 | Interviews[interviewID.ToLower()] = newInterview; 283 | 284 | newInterview.InterviewID = interviewID; 285 | newInterview.Status = InterviewStatus.SelectingPositions; 286 | newInterview.Players.AddRange(oldInterview.Players); 287 | newInterview.InterviewerIndex = oldInterview.InterviewerIndex; 288 | newInterview.SuspectIndex = oldInterview.SuspectIndex; 289 | 290 | return newInterview; 291 | } 292 | 293 | private void LogInterview(Interview interview) 294 | { 295 | var duration = interview.Started.HasValue 296 | ? DateTime.Now - interview.Started.Value 297 | : TimeSpan.Zero; 298 | 299 | var interviewData = new 300 | { 301 | interview.Status, 302 | interview.Outcome, 303 | Duration = duration, 304 | Packet = interview.Packet.Name, 305 | InterferencePattern = interview.InterferencePattern.ToString(), 306 | InterferenceSolution = interview.InterferencePattern.SolutionSequence, 307 | SuspectBackground = interview.SuspectBackgrounds.First(), 308 | SuspectType = interview.Role.Type, 309 | SuspectFault = interview.Role.Fault, 310 | SuspectTraits = interview.Role.Traits, 311 | }; 312 | 313 | var options = new JsonSerializerOptions(); 314 | options.Converters.Add(new JsonStringEnumConverter()); 315 | var strData = JsonSerializer.Serialize(interviewData, options); 316 | 317 | Logger.LogInformation( 318 | "Interview completed at {Time}: {Data}", 319 | DateTime.Now, 320 | strData 321 | ); 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /RobotInterrogation/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.HttpsPolicy; 4 | using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using RobotInterrogation.Hubs; 9 | using RobotInterrogation.Models; 10 | using RobotInterrogation.Services; 11 | using System.Text.Json.Serialization; 12 | 13 | namespace RobotInterrogation 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.AddMvc() 28 | .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); 29 | 30 | // In production, the React files will be served from this directory 31 | services.AddSpaStaticFiles(configuration => 32 | { 33 | configuration.RootPath = "ClientApp/build"; 34 | }); 35 | 36 | services.Configure(options => Configuration.GetSection("GameConfiguration").Bind(options)); 37 | services.Configure(options => Configuration.GetSection("IDGeneration").Bind(options)); 38 | 39 | services.AddScoped(); 40 | services.AddScoped(); 41 | 42 | services.AddSignalR() 43 | .AddJsonProtocol(options => options.PayloadSerializerOptions.Converters.Add(new JsonStringEnumConverter())); 44 | } 45 | 46 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 47 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 48 | { 49 | if (env.IsDevelopment()) 50 | { 51 | app.UseDeveloperExceptionPage(); 52 | } 53 | else 54 | { 55 | app.UseExceptionHandler("/Error"); 56 | app.UseHsts(); 57 | } 58 | 59 | app.UseHttpsRedirection(); 60 | app.UseStaticFiles(); 61 | app.UseSpaStaticFiles(); 62 | 63 | app.UseRouting(); 64 | 65 | app.UseEndpoints(endpoints => 66 | { 67 | endpoints.MapHub("/hub/Interview"); 68 | 69 | endpoints.MapControllerRoute( 70 | name: "default", 71 | pattern: "{controller}/{action=Index}/{id?}"); 72 | }); 73 | 74 | app.UseSpa(spa => 75 | { 76 | spa.Options.SourcePath = "ClientApp"; 77 | 78 | if (env.IsDevelopment()) 79 | { 80 | spa.UseReactDevelopmentServer(npmScript: "start"); 81 | } 82 | }); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /RobotInterrogation/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/InterferenceServiceTests.cs: -------------------------------------------------------------------------------- 1 | using RobotInterrogation.Services; 2 | using System; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace Tests 7 | { 8 | public class InterferenceServiceTests 9 | { 10 | [Theory] 11 | [InlineData(1, 7, 4, 8)] 12 | [InlineData(2, 4, 8, 4)] 13 | [InlineData(3, 7, 4, 6)] 14 | 15 | public void GeneratePattern_ExpectedDimensions(int seed, int width, int height, int numMarkers) 16 | { 17 | var random = new Random(seed); 18 | var service = new InterferenceService(); 19 | 20 | var pattern = service.Generate(random, width, height, numMarkers); 21 | Assert.NotNull(pattern); 22 | 23 | var display = pattern.ToString(); 24 | Assert.NotNull(display); 25 | 26 | var displayLines = display.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); 27 | Assert.Equal(height * 4 + 2, displayLines.Length); 28 | Assert.Equal(string.Empty, displayLines.Last()); 29 | 30 | foreach (var line in displayLines.Take(displayLines.Length - 1)) 31 | { 32 | Assert.Equal(width * 4 + 1, line.Length); 33 | } 34 | 35 | Assert.Equal(numMarkers, pattern.MarkerSequence.Count); 36 | Assert.Equal(numMarkers, pattern.SolutionSequence.Count); 37 | Assert.Equal(numMarkers, pattern.Arrows.Count); 38 | } 39 | 40 | [Theory] 41 | [InlineData(1, "EFACBD", @" 42 | ┌───┬───────────┐ 43 | │ │ │ 44 | │ │ │ ────┬───┐ │ 45 | │↑│ │ │ │ │ 46 | │ │ ├──── │ │ │ │ 47 | │B│ │ │ │C│ │ 48 | │ │ │ ┌───┘ │ │ │ 49 | │ │ │ │ │↑│ │ 50 | │ │ │ │ ┌───┤ │ │ 51 | │ │ │ │A │ │ │ 52 | │ └───┘ │ │ │ │ │ 53 | │ │↑│ │ │D│ 54 | ├───────┘ │ │ │ │ 55 | │ │ │↓│ 56 | │ ┌───────┴───┘ │ 57 | │ │ │ 58 | │ │ ────┬───────┤ 59 | │ │ │ E│ 60 | │ └───┐ │ ┌───┐ │ 61 | │ │ │ │ │↓│ 62 | ├───┐ │ │ │ │ │ │ 63 | │ │ │ │ │ │ │ │ 64 | │ │ │ │ │ │ │ │ │ 65 | │ │ │ │ │ │ │ │ 66 | │ │ │ │ │ │ └───┤ 67 | │ │ │ │ │ │ │ 68 | │ │ │ │ │ └───┐ │ 69 | │↑│ │ │ │ │ │ 70 | │ │ │ │ └──── │ │ 71 | │F│ │ │ │ 72 | │ └───┴───────┘ │ 73 | │ │ 74 | └───────────────┘ 75 | ")] 76 | [InlineData(2, "BFEDAC", @" 77 | ┌───┬───────────┐ 78 | │ │ │ 79 | │ │ │ ┌───────┐ │ 80 | │ │ │ │ │ │ 81 | │ │ │ │ ────┐ │ │ 82 | │ │ │ │A → │ │ │ 83 | │ │ │ ├───┐ │ │ │ 84 | │ │ │ │ │ │ │ 85 | │ │ │ │ │ │ └───┤ 86 | │ │ │ │ │ │ │ 87 | │ │ │ │ │ └───┐ │ 88 | │ │ │ │ ← D│ │ 89 | │ │ ├───┴───┐ │ │ 90 | │ │ │ │ │ │ 91 | │ │ │ ┌───┐ │ │ │ 92 | │ │E│ │ │ │ │ │ 93 | │ │ │ │ │ │ │ │ │ 94 | │ │↓│ │ │F│ │ │ 95 | │ │ │ │ │ └───┤ │ 96 | │ │ │ │ │↑ │↓│ 97 | │ │ │ │ ├──── │ │ 98 | │ │ │ │ │C│ 99 | │ └───┘ │ ┌───┘ │ 100 | │ │ │ │ 101 | ├───────┤ │ ────┤ 102 | │ → B │ │ │ 103 | │ ┌──── │ └───┐ │ 104 | │ │ │ │ │ 105 | │ │ ────┴──── │ │ 106 | │ │ │ │ 107 | │ └───────────┘ │ 108 | │ │ 109 | └───────────────┘ 110 | ")] 111 | [InlineData(3, "CEBADF", @" 112 | ┌───────┬───────┐ 113 | │ │ │ 114 | │ ────┐ │ ────┐ │ 115 | │ │↑│ │ │ 116 | ├───┐ │ └──── │ │ 117 | │ │ │A │ │ 118 | │ │ │ └───────┤ │ 119 | │ │ │ │ │ 120 | │ │ └───┬──── │ │ 121 | │ │ │ │↑│ 122 | │ ├───┐ │ ┌───┘ │ 123 | │ │ D│ │ │ B│ 124 | │ │ │ │ │ │ ────┤ 125 | │F│ │↓│ │ │ │ 126 | │ │ │ │ │ └───┐ │ 127 | │↓│ │ │ │ │ 128 | │ │ └───┴──── │ │ 129 | │ │ │ │ 130 | │ ├───────────┘ │ 131 | │ │ │ 132 | │ │ ────────┬───┤ 133 | │ │ ← E │ │ 134 | │ └───────┐ │ │ │ 135 | │ │ │ │ │ 136 | ├──────── │ │ │ │ 137 | │ │ │ │ │ 138 | │ ┌───────┘ │ │ │ 139 | │ │ → C │ │ │ 140 | │ │ ────────┘ │ │ 141 | │ │ │ │ 142 | │ └───────────┘ │ 143 | │ │ 144 | └───────────────┘ 145 | ")] 146 | public void GeneratePattern_SpecificResults(int seed, string expectedSequence, string expectedLayout) 147 | { 148 | var random = new Random(seed); 149 | var service = new InterferenceService(); 150 | 151 | var pattern = service.Generate(random, 4, 8, 6); 152 | Assert.NotNull(pattern); 153 | 154 | var display = pattern.ToString(); 155 | Assert.NotNull(display); 156 | 157 | string actualSequence = string.Join(string.Empty, pattern.SolutionSequence); 158 | Assert.Equal(expectedSequence, actualSequence); 159 | 160 | Assert.Equal(expectedLayout.Trim(), display.Trim()); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------