├── .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 | You need to enable JavaScript to run this app.
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 | Go back
46 |
47 |
48 |
49 | Connection problems?
50 | {toggleText}
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 | Information
19 | Start a game
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 | Go back
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 | props.continue()}>Continue
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 | props.correct()}>Correct response
34 | props.incorrect()}>Incorrect response
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 | Play again
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 |
58 | Suspect is Human
59 |
60 |
64 | Suspect is a Robot
65 |
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 | Remain as the Interviewer
26 | Switch positions and become the Suspect
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 | Start the Timer
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 | Go back
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 | props.confirm!()}>Continue
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 | {props.options[0]}
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 | props.action(0)}>{props.options[0]}
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 | Kill interviewer
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 {val}
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 |
50 | {midRow}
51 | ↓
52 |
53 | {lastRow}
54 |
55 |
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 |
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 |
--------------------------------------------------------------------------------