├── requirements.txt ├── logo.png ├── utils.py ├── .gitignore ├── deck_manager.py ├── LICENSE.md ├── member.py ├── README.md ├── main.py ├── game_manager.py ├── style.css ├── index.html ├── iic_cards.json └── script.js /requirements.txt: -------------------------------------------------------------------------------- 1 | websockets==8.1 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iwotastic/internetcards/HEAD/logo.png -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | async def send_object(ws, obj): 4 | await ws.send(json.dumps(obj)) 5 | 6 | async def recv_object(ws): 7 | data = await ws.recv() 8 | try: 9 | res = json.loads(data) 10 | return res 11 | except json.JSONDecodeError as e: 12 | return {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # VENV directories 2 | .Python 3 | [Bb]in 4 | [Ii]nclude 5 | [Ll]ib 6 | [Ll]ib64 7 | [Ll]ocal 8 | [Ss]cripts 9 | pyvenv.cfg 10 | .venv 11 | pip-selfcheck.json 12 | 13 | # VSCode 14 | .vscode 15 | 16 | # Python general 17 | __pycache__ 18 | 19 | # Project related 20 | deploy.py 21 | *.afdesign 22 | iic_cards.csv 23 | form_csv_sorter.py -------------------------------------------------------------------------------- /deck_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | class DeckManager: 4 | __default = False 5 | 6 | @staticmethod 7 | def default(): 8 | if DeckManager.__default: 9 | return DeckManager.__default 10 | else: 11 | DeckManager.__default = DeckManager() 12 | return DeckManager.__default 13 | 14 | def __init__(self): 15 | self.packs = None 16 | with open("./iic_cards.json", "r") as cardFile: 17 | self.packs = json.load(cardFile) 18 | 19 | def listPacks(self): 20 | return self.packs.keys() 21 | 22 | def getQuestions(self, pack): 23 | return self.packs[pack]["questions"] 24 | 25 | def getAnswers(self, pack): 26 | return self.packs[pack]["answers"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ian Morrill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /member.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from utils import send_object, recv_object 3 | 4 | class Member: 5 | def __init__(self, ws, name): 6 | self.ws = ws 7 | self.name = name 8 | 9 | self.hand = [] 10 | self.handLimit = 7 11 | 12 | self.score = 0 13 | 14 | self.queuedBroadcasts = asyncio.Queue() 15 | self.queuedReplys = asyncio.Queue() 16 | 17 | self.chatMessageHandler = None 18 | 19 | def fillHand(self, deck): 20 | while len(self.hand) < self.handLimit: 21 | self.hand.append(deck.pop()) 22 | 23 | async def addObj(self, obj): 24 | await self.queuedBroadcasts.put(obj) 25 | 26 | async def getObj(self): 27 | return await self.queuedReplys.get() 28 | 29 | async def manualBroadcast(self): 30 | await send_object(self.ws, await self.queuedBroadcasts.get()) 31 | 32 | async def broadcastLoop(self): 33 | while True: 34 | await self.manualBroadcast() 35 | 36 | async def manualRead(self): 37 | obj = await recv_object(self.ws) 38 | if "action" in obj and obj["action"] == "sendChat" and self.chatMessageHandler != None: 39 | await self.chatMessageHandler(obj, self) 40 | else: 41 | await self.queuedReplys.put(obj) 42 | 43 | async def readLoop(self): 44 | while True: 45 | await self.manualRead() 46 | 47 | async def runGameLoop(self): 48 | broadcastLoop = asyncio.ensure_future(self.broadcastLoop()) 49 | readLoop = asyncio.ensure_future(self.readLoop()) 50 | 51 | done, pending = await asyncio.wait([broadcastLoop, readLoop], return_when=asyncio.FIRST_COMPLETED) 52 | for task in pending: 53 | task.cancel() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
Ian's Internet Cards
2 |
An internet-based, Cards Against Humanity-inspired party game.
3 |
4 | Play It! | 5 | Report A Bug 6 |
7 | 8 | --- 9 | 10 | ## Running Locally 11 | Clone the repo to your own computer and run the following (or the equivalent if on Windows) to set up Ian's Internet Cards: 12 | ```bash 13 | # Initialize the venv 14 | python3 -m venv . 15 | 16 | # Activate the venv 17 | source ./bin/activate 18 | 19 | # Install requirements with pip 20 | pip install -r ./requirements.txt 21 | ``` 22 | 23 | To run Ian's Internet Cards on your own machine run the following: 24 | ```bash 25 | # Start the webserver 26 | python3 -m http.server 8000 27 | 28 | # Start the WebSockets server (in another terminal) 29 | python3 main.py 30 | 31 | # Now you can go to http://127.0.0.1:8000 to try it out. 32 | ``` 33 | 34 | ## Code Organization 35 | | Files | What do they do? | 36 | |-------|---------| 37 | | `iic_cards.json` | Provides raw machine-readable cards for Ian's Internet Cards. | 38 | | `index.html`, `script.js`, `style.css` | Files loaded by the web that communicate with the Python-based WebSockets server. | 39 | | `main.py` | The file run to start the WebSockets server. | 40 | | `member.py` | A class that encapsulates all state of a member of a game room. | 41 | | `deck_manager.py` | A singleton that keeps the main deck from `iic_cards.json` in memory. | 42 | | `game_manager.py` | A class that encapsulates game state and is associated with the creator of a room. | 43 | | `utils.py` | Contains utility function that make sending JSON data less annoying. | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | from game_manager import GameManager 4 | from member import Member 5 | from utils import send_object, recv_object 6 | 7 | games = dict() 8 | 9 | async def handleRoomCreation(ws, initObj, setRoom): 10 | if "name" in initObj and initObj["name"].strip() != "": 11 | gm = GameManager(initObj["packs"], initObj["questions"], initObj["answers"]) 12 | 13 | while gm.joinCode in games: 14 | gm.regenJoinCode() 15 | 16 | m = Member(ws, initObj["name"]) 17 | gm.addMember(m) 18 | games[gm.joinCode] = gm 19 | setRoom(gm.joinCode) 20 | await send_object(ws, {"action": "roomMade", "joinCode": gm.joinCode, "setName": initObj["name"]}) 21 | nextObj = await recv_object(ws) 22 | 23 | if "action" in nextObj: 24 | if nextObj["action"] == "startGame": 25 | await asyncio.wait([games[gm.joinCode].startGame(), m.runGameLoop()], return_when=asyncio.FIRST_COMPLETED) 26 | else: 27 | await send_object(ws, {"action": "error", "content": "Please make sure to set a name."}) 28 | 29 | async def handleRoomJoin(ws, initObj, setRoom): 30 | if "name" in initObj and initObj["name"].strip() != "": 31 | if "joinCode" in initObj and len(initObj["joinCode"]) == 6: 32 | if initObj["joinCode"] in games and games[initObj["joinCode"]].status == "open": 33 | for member in games[initObj["joinCode"]].members: 34 | await send_object(member.ws, { 35 | "action": "memberJoined", 36 | "name": initObj["name"] 37 | }) 38 | await send_object(ws, { 39 | "action": "roomJoined", 40 | "members": [m.name for m in games[initObj["joinCode"]].members], 41 | "setName": initObj["name"], 42 | "joinCode": initObj["joinCode"] 43 | }) 44 | setRoom(initObj["joinCode"]) 45 | 46 | m = Member(ws, initObj["name"]) 47 | games[initObj["joinCode"]].addMember(m) 48 | await m.runGameLoop() 49 | else: 50 | await send_object(ws, {"action": "error", "content": "This room does not exist."}) 51 | else: 52 | await send_object(ws, {"action": "error", "content": "Please make sure to specify a valid room code."}) 53 | else: 54 | await send_object(ws, {"action": "error", "content": "Please make sure to set a name."}) 55 | 56 | async def serve(ws, path): 57 | currentRoom = "" 58 | 59 | def setRoom(joinCode): 60 | nonlocal currentRoom 61 | currentRoom = joinCode 62 | 63 | try: 64 | while True: 65 | initObj = await recv_object(ws) 66 | if "action" in initObj: 67 | if initObj["action"] == "makeRoom": 68 | await handleRoomCreation(ws, initObj, setRoom) 69 | elif initObj["action"] == "joinRoom": 70 | await handleRoomJoin(ws, initObj, setRoom) 71 | else: 72 | await send_object(ws, {"action": "error", "content": "Invalid action."}) 73 | except websockets.ConnectionClosed as e: 74 | if currentRoom != "": 75 | if games[currentRoom].members[0].ws == ws: 76 | games[currentRoom].members.pop(0) 77 | await games[currentRoom].broadcastToAll({"action": "hostLeft"}) 78 | else: 79 | index = 0 80 | for member in games[currentRoom].members: 81 | if member.ws == ws: 82 | break 83 | index += 1 84 | await games[currentRoom].broadcastToAll({"action": "memberLeft", "name": games[currentRoom].members.pop(index).name}) 85 | else: 86 | print("Whoops") 87 | 88 | finally: 89 | pass 90 | 91 | start_server = websockets.serve(serve, "127.0.0.1", 8765) 92 | 93 | asyncio.get_event_loop().run_until_complete(start_server) 94 | asyncio.get_event_loop().run_forever() -------------------------------------------------------------------------------- /game_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from random import randint, shuffle 3 | from utils import send_object, recv_object 4 | from deck_manager import DeckManager 5 | 6 | class GameManager: 7 | def __init__(self, packs, questions=[], answers=[]): 8 | self.joinCode = "".join(str(randint(0, 9)) for i in range(0, 6)) 9 | self.members = [] 10 | self.questions = [] 11 | self.answers = [] 12 | self.customQuestions = questions 13 | self.customAnswers = answers 14 | self.question = "" 15 | self.status = "open" 16 | self.judge = 0 17 | self.packs = packs 18 | 19 | def regenJoinCode(self): 20 | self.joinCode = "".join(str(randint(0, 9)) for i in range(0, 6)) 21 | 22 | def addMember(self, member): 23 | memberId = len(self.members) 24 | member.chatMessageHandler = self.handleChatMessage 25 | self.members.append(member) 26 | return memberId 27 | 28 | async def broadcastToAll(self, obj): 29 | for member in self.members: 30 | await member.addObj(obj) 31 | 32 | async def broadcastToAllExcept(self, doNotInclude, obj): 33 | for member in self.members: 34 | if doNotInclude != member: 35 | await member.addObj(obj) 36 | 37 | async def handleChatMessage(self, obj, member): 38 | await self.broadcastToAllExcept(member, { 39 | "action": "chatMessage", 40 | "from": member.name, 41 | "content": obj["content"] 42 | }) 43 | 44 | def setUpDecks(self, categories): 45 | self.questions = self.customQuestions 46 | self.answers = self.customAnswers 47 | for category in categories: 48 | self.questions = self.questions + DeckManager.default().getQuestions(category) 49 | self.answers = self.answers + DeckManager.default().getAnswers(category) 50 | shuffle(self.questions) 51 | shuffle(self.answers) 52 | 53 | def fillAllHands(self): 54 | for member in self.members: 55 | if len(self.answers) < 1: 56 | self.setUpDecks(self.packs) 57 | member.fillHand(self.answers) 58 | 59 | async def getAnswerFromMember(self, member): 60 | return {"answerObj": await member.getObj(), "member": member} 61 | 62 | async def getAllAnswers(self): 63 | membersToWaitFor = self.members[:self.judge] + self.members[self.judge + 1:] 64 | rawAnswerData = await asyncio.gather(*[self.getAnswerFromMember(m) for m in membersToWaitFor]) 65 | 66 | result = [] 67 | for answer in rawAnswerData: 68 | result.append({"answer": answer["answerObj"]["answer"], "member": answer["member"]}) 69 | 70 | return result 71 | 72 | async def getJudgeSelection(self): 73 | return await self.members[self.judge].getObj() 74 | 75 | async def sendOutNewQuestion(self): 76 | self.fillAllHands() 77 | 78 | if len(self.questions) == 0: 79 | self.setUpDecks(self.packs) 80 | 81 | self.question = self.questions.pop().replace("[[BLANK]]", "___") 82 | 83 | for member in self.members: 84 | if member != self.members[self.judge]: 85 | await member.addObj({ 86 | "action": "newQuestion", 87 | "question": self.question, 88 | "hand": member.hand, 89 | "judge": self.members[self.judge].name 90 | }) 91 | else: 92 | await member.addObj({ 93 | "action": "youAreTheJudge", 94 | "question": self.question 95 | }) 96 | 97 | def clearSelectedCards(self, answerData): 98 | for answer in answerData: 99 | if answer["answer"] in answer["member"].hand: 100 | answer["member"].hand.remove(answer["answer"]) 101 | 102 | async def startGame(self): 103 | await self.broadcastToAll({"action": "preparingGame"}) 104 | 105 | self.judge = randint(0, len(self.members) - 1) 106 | self.status = "ingame" 107 | self.setUpDecks(self.packs) 108 | 109 | await self.broadcastToAll({"action": "gameStart"}) 110 | 111 | while True: 112 | await self.sendOutNewQuestion() 113 | 114 | # gets all answer data as objects 115 | answerData = await self.getAllAnswers() 116 | 117 | # Begin judging 118 | answersToDisplay = [answer["answer"] for answer in answerData] 119 | shuffle(answersToDisplay) 120 | await self.broadcastToAllExcept(self.members[self.judge], { 121 | "action": "showAnswers", 122 | "answers": answersToDisplay, 123 | "question": self.question, 124 | "judge": self.members[self.judge].name 125 | }) 126 | await self.members[self.judge].addObj({ 127 | "action": "selectAnswer", 128 | "answers": answersToDisplay, 129 | "question": self.question 130 | }) 131 | judgeSelection = await self.getJudgeSelection() 132 | 133 | # Figure out who the judge picked 134 | pickedMember = self.members[0] 135 | for answer in answerData: 136 | if answer["answer"] == judgeSelection["answer"]: 137 | pickedMember = answer["member"] 138 | break 139 | 140 | # Send out selection data 141 | await self.broadcastToAll({"action": "judgeSelected", "answer": judgeSelection["answer"], "question": self.question, "submittedBy": pickedMember.name}) 142 | 143 | # Send out score data 144 | pickedMember.score += 1 145 | await pickedMember.addObj({ 146 | "action": "updateScore", 147 | "score": pickedMember.score 148 | }) 149 | 150 | # Wait a bit 151 | await asyncio.sleep(5) 152 | 153 | # Get ready for next round 154 | self.clearSelectedCards(answerData) 155 | self.judge = (self.judge + 1) % len(self.members) 156 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* Basic Resets */ 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 7 | } 8 | 9 | * { 10 | box-sizing: border-box; 11 | } 12 | 13 | /* Typography */ 14 | 15 | p { 16 | font-size: 15pt; 17 | } 18 | 19 | .logo { 20 | margin-top: 3pt; 21 | width: 300px; 22 | } 23 | 24 | .logo + p { 25 | margin-top: 0; 26 | } 27 | 28 | h2 { 29 | margin: 10px; 30 | color: #CB4335; 31 | } 32 | 33 | .blue-question { 34 | margin-top: 15px; 35 | color: #2E86C1; 36 | } 37 | 38 | .heading-no-margin { 39 | margin-top: 0; 40 | } 41 | 42 | .status-bar { 43 | background: #d0d0d0; 44 | text-align: center; 45 | font-size: 16pt; 46 | padding-bottom: 3pt; 47 | padding-left: 4.5pt; 48 | } 49 | 50 | /* Layout */ 51 | 52 | .app { 53 | margin: 0; 54 | padding: 0; 55 | display: flex; 56 | flex-direction: column; 57 | width: 100vw; 58 | height: 100vh; 59 | } 60 | 61 | .navbar { 62 | background: linear-gradient(-45deg, #CB4335, #2E86C1); 63 | color: #fff; 64 | display: flex; 65 | } 66 | 67 | .navbar .left { 68 | font-size: 16pt; 69 | padding: 10pt; 70 | display: block; 71 | flex: 1; 72 | } 73 | 74 | .navbar .right { 75 | font-size: 16pt; 76 | padding: 10pt; 77 | display: block; 78 | } 79 | 80 | .content { 81 | display: flex; 82 | flex-direction: row; 83 | flex: 1; 84 | } 85 | 86 | .content > .sidebar { 87 | display: flex; 88 | flex-direction: column; 89 | width: 200pt; 90 | background-color: #dddddd; 91 | } 92 | 93 | @media (max-width: 550px) { 94 | .content:not(.sidebar-hidden) > .scene { 95 | display: none; 96 | } 97 | 98 | .content > .sidebar { 99 | display: block; 100 | flex: 1; 101 | width: unset; 102 | background-color: #dddddd; 103 | } 104 | } 105 | 106 | .content.sidebar-hidden > .sidebar { 107 | display: none; 108 | } 109 | 110 | .scene { 111 | text-align: center; 112 | margin: 0; 113 | padding: 0; 114 | flex: 1; 115 | width: 0; 116 | min-width: 0; 117 | overflow-y: scroll; 118 | } 119 | 120 | .scene:not([data-active-scene]) { 121 | display: none; 122 | } 123 | 124 | .scene.no-center { 125 | text-align: left; 126 | } 127 | 128 | .row { 129 | width: 100%; 130 | display: flex; 131 | padding: 5pt; 132 | justify-content: center; 133 | column-gap: 5pt; 134 | } 135 | 136 | .row > * { 137 | width: 250pt; 138 | } 139 | 140 | .row > div > div { 141 | text-align: left; 142 | } 143 | 144 | @media (max-width: 511pt) { 145 | .row { 146 | display: block; 147 | } 148 | 149 | .row > * { 150 | margin-left: auto; 151 | margin-right: auto; 152 | } 153 | } 154 | 155 | @media (min-width: 511pt) { 156 | .row > *:not(:first-child) { 157 | border-left: #888 1pt solid; 158 | padding-left: 5pt; 159 | } 160 | } 161 | 162 | .footer { 163 | position: fixed; 164 | bottom: 0; 165 | left: 0; 166 | width: 100vw; 167 | padding: 4pt; 168 | background: #dddddd; 169 | font-size: 10pt; 170 | text-align: center; 171 | } 172 | 173 | /* Input boxes */ 174 | 175 | input:not([type=checkbox]) { 176 | width: 150pt; 177 | border: 2pt #bbb solid; 178 | border-radius: 5pt; 179 | font-size: 12pt; 180 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 181 | padding: 5pt; 182 | } 183 | 184 | input:not([type=checkbox]):focus { 185 | border: 2pt #000 solid; 186 | outline: none; 187 | } 188 | 189 | /* Buttons */ 190 | 191 | .btn-row { 192 | margin-top: 7pt; 193 | } 194 | 195 | .btn { 196 | padding: 5pt; 197 | border-radius: 6pt; 198 | border: none; 199 | border-bottom: 3pt #2471A3 solid; 200 | font: 12pt "Helvetica Neue", Helvetica, sans-serif; 201 | background: #2E86C1; 202 | box-shadow: 0 3pt 3pt #000; 203 | transition: all 0.2s; 204 | color: #fff; 205 | } 206 | 207 | .btn:hover { 208 | box-shadow: 0 0 0pt #000; 209 | border-bottom: 3pt #2471A3 solid; 210 | transform: translatey(3pt); 211 | cursor: pointer; 212 | } 213 | 214 | .btn:active { 215 | border-bottom: 0pt #999 solid; 216 | transform: translatey(6pt); 217 | } 218 | 219 | .btn.danger { 220 | padding: 5pt; 221 | border-radius: 6pt; 222 | border: none; 223 | border-bottom: 3pt #A93226 solid; 224 | font: 12pt "Helvetica Neue", Helvetica, sans-serif; 225 | background: #CB4335; 226 | box-shadow: 0 3pt 3pt #000; 227 | transition: all 0.2s; 228 | color: #fff; 229 | } 230 | 231 | .btn.danger:hover { 232 | box-shadow: 0 0 0pt #000; 233 | border-bottom: 3pt #A93226 solid; 234 | transform: translatey(3pt); 235 | cursor: pointer; 236 | } 237 | 238 | .btn.danger:active { 239 | border-bottom: 0pt #999 solid; 240 | transform: translatey(6pt); 241 | } 242 | 243 | /* Cards */ 244 | 245 | .card-list { 246 | width: 100%; 247 | height: 180pt; 248 | display: flex; 249 | overflow-y: scroll; 250 | overflow-x: visible; 251 | padding: 0; 252 | } 253 | 254 | .card-list > div { 255 | padding: 0; 256 | margin: 0; 257 | } 258 | 259 | .card-list > button { 260 | padding: 0; 261 | margin: 0; 262 | background: transparent; 263 | border: none; 264 | } 265 | 266 | .card-list > button > div { 267 | padding: 5pt; 268 | margin: 5pt; 269 | height: 170pt; 270 | width: 100pt; 271 | border-radius: 5pt; 272 | background: linear-gradient(-45deg, #A93226, #CB4335); 273 | text-align: left; 274 | color: #fff; 275 | font: 12pt "Helvetica Neue", Helvetica, sans-serif; 276 | transition: all 0.2s; 277 | } 278 | 279 | .card-list > button:hover > div { 280 | transform: translatey(-5pt); 281 | box-shadow: 0 5pt 3pt #333; 282 | cursor: pointer; 283 | } 284 | 285 | .card-list > button:active > div { 286 | transform: translatey(0pt) scale(0.9); 287 | box-shadow: none; 288 | cursor: pointer; 289 | } 290 | 291 | .card { 292 | padding: 5pt; 293 | margin: 5pt; 294 | height: 170pt; 295 | width: 100pt; 296 | border-radius: 5pt; 297 | text-align: left; 298 | color: #fff; 299 | font: 12pt "Helvetica Neue", Helvetica, sans-serif; 300 | } 301 | 302 | .card.red { 303 | background: linear-gradient(-45deg, #A93226, #CB4335); 304 | } 305 | 306 | .card.blue { 307 | background: linear-gradient(-45deg, #2471A3, #2E86C1); 308 | } 309 | 310 | .inline-group { 311 | display: inline-flex; 312 | } 313 | 314 | /* Checkboxes */ 315 | 316 | .checkboxes > label { 317 | display: inline-block; 318 | padding: 5pt; 319 | border-radius: 5pt; 320 | margin: 0; 321 | margin-left: 5pt; 322 | background-color: #eeeeee; 323 | cursor: pointer; 324 | } 325 | 326 | .checkboxes > label > input { 327 | margin-left: 5pt; 328 | } 329 | 330 | .checkboxes > .btn { 331 | padding: 5pt; 332 | border-radius: 5pt; 333 | margin: 0; 334 | margin-left: 5pt; 335 | background-color: #8db9d6; 336 | cursor: pointer; 337 | box-shadow: none; 338 | border: none; 339 | color: #0f3955; 340 | } 341 | 342 | .checkboxes > .btn:hover { 343 | background-color: #2471A3; 344 | transform: none; 345 | color: #fff; 346 | } 347 | 348 | .checkboxes > .btn:active { 349 | background-color: #2E86C1; 350 | transform: none; 351 | color: #fff; 352 | } 353 | 354 | /* Bottom bar */ 355 | 356 | .bottom-bar { 357 | position: fixed; 358 | bottom: 0; 359 | right: 10pt; 360 | padding: 5pt; 361 | border-top-left-radius: 5pt; 362 | border-top-right-radius: 5pt; 363 | background: #2E86C1; 364 | } 365 | 366 | .bottom-bar.hidden { 367 | display: none; 368 | } 369 | 370 | .bottom-bar > button { 371 | padding: 5pt; 372 | border-radius: 5pt; 373 | border: 0; 374 | font: 12pt "Helvetica Neue", Helvetica, sans-serif; 375 | color: #fff; 376 | background: #2471A3; 377 | cursor: pointer; 378 | } 379 | 380 | /* Chat messgaes */ 381 | 382 | .message-title { 383 | display: flex; 384 | } 385 | 386 | .message-title > span { 387 | font-size: 17pt; 388 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 389 | padding: 5pt; 390 | font-weight: bold; 391 | flex: 1; 392 | } 393 | 394 | .message-list { 395 | flex: 1; 396 | overflow-y: scroll; 397 | } 398 | 399 | .message { 400 | padding: 5pt; 401 | } 402 | 403 | .message-compose { 404 | height: 50pt; 405 | width: 100%; 406 | display: flex; 407 | flex-direction: row; 408 | } 409 | 410 | .message-compose > input { 411 | flex: 1; 412 | width: unset; 413 | } 414 | 415 | .message-compose > button { 416 | border: 2pt #bbb solid; 417 | border-radius: 5pt; 418 | font-size: 12pt; 419 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 420 | padding: 5pt; 421 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ian's Internet Cards 7 | 8 | 9 | 10 |
11 | 15 | 16 | 195 |
196 | 197 | 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /iic_cards.json: -------------------------------------------------------------------------------- 1 | {"PG-13": {"answers": ["My crippling loneliness ", "The auto-immune deficiency syndrome", "A pubic hair", "My hairy palms", "Ticks all over your body", "A paper cut on the tip of my penis", "Stepping in a dog turd", "Pierced nipples", "Urine", "Having my blood harvested by our mosquito overlords", "Turd graffiti", "An inopportune sharting", "The purge", "Pooping on the streets", "Virginity", "Penis enlargement pills", "Making my anus bleed", "Kidney stones", "Taking off my pants in a shopping mall", "Yellow stained underwear ", "My scrotum sticking to the inside of my leg", "Biggus Dickus", "Semen", "Urinating all over my house", "Shaving my grandma\u2019s legs", "A poodle eating my insides", "Opioids ", "Killing someone with a spoon", "Eclairs filled with diarrhea instead of chocolate", "Stabbing myself with a butter knife ", "Depression ", "A jockstrap with bells on it", "Nonstop diarrhea ", "Making a dog eat dark chocolate", "Drinking toilet water", "An ova", "Sprite Cranberry", "Meat shafts", "Picking boogers and licking them", "Lice in my pubic hair", "Projectile diarrhea", "Staring at the sun until my eyes melt", "Sperm", "Colonoscopies", "Xi Jinping's left testicle", "Zoom dating", "My questionable sexuality", "Hitler\u2019s missing ball", "Killing EVERYONE!!!", "Man spreading", "18 naked cowboys in the showers", "The hostages", "N95 masks laced with chloroform", "High resolution 3D scans of people's feet", "Extra Chromosomes", "My Gandalf the Grey body pillow", "Lack of rights ", "The hole that I somehow burned in crotch of my pants that I wear to Boy Scout meetings", "Unfortunately, Zeus was horny", "My \"Fun Sized\" Penis.", "The STDs that I gave to your mom", "The penis veins on Snickers bars.", "Giving guns human rights", "Airborne AIDS", "Pepperoni nips", "Illumination Studio's worst movie \"HOP\"", "Robert E Lee", "Communism done right", "There is no meme. Uhh. Take off your clothes.", "3 gallons of milk, 2 gallons of gasoline, and above the legal limit of alcohol", "September 11th, 2001", "Inbreeding", "Unjust suffering", "Glands", "Abortion", "My body, my choice", "Sempai", "Survivor's guilt", "Florida man", "Viscous fluid"], "questions": ["This quarantine really got me horny for [[BLANK]].", "Most people use their pantry to store food, I use mine to store [[BLANK]].", "Ew, I just stepped in [[BLANK]].", "While I was in the bathroom I saw [[BLANK]].", "When living with [[BLANK]] i want to die.", "I found the Hulk doing terrible things to [[BLANK]].", "If I had to get an STD I would like it to be [[BLANK]].", "Before I die I want to see [[BLANK]].", "A RPG loaded with [[BLANK]].", "The house next door to me was sold to [[BLANK]].", "If I could I would kill my [[BLANK]].", "Like shooting [[BLANK]] in a barrel.", "Doctor! Come quick, it's my [[BLANK]].", "Oh my god, that [[BLANK]] is so big!", "The 10th circle of Hell contains [[BLANK]].", "Damnit, the internet went out! What am I supposed to do without [[BLANK]]?", "Mom says it's my turn on the [[BLANK]].", "If you were any more inbred you'd be [[BLANK]].", "Does the name [[BLANK]] mean anything to you?", "Mom, Dad, there's something I need to tell you: I'm [[BLANK]].", "We will build a great, great, [[BLANK]].", "Lets get ready for [[BLANK]].", "The only thing better then the third reich would be [[BLANK]].", "I did not have sexual relations with [[BLANK]]", "I drink to forget [[BLANK]]", "Go commit [[BLANK]].", "Do you like the [[BLANK]] fanfic I just made?"]}, "PG": {"answers": ["Copious amounts of milk", "Shrek 2", "Coronacation ", "A dead meme", "A stockpile of N95 masks", "Baking cookies in calculus", "A sentient pineapple", "University of Maryland \u2014 College Park", "The plague", "One million ants", "A smelly fart", "Grandma", "Girl Scout cookies", "Listerine and 7 tongue depressors ", "Autocorrect", "A pillow fight", "237 Cats", "Extreme nose hair", "Boogers smeared under the desk", "Public defecation", "Being covered in dirty underpants", "A pervert looking at you", "Eggs", "A terrorist", "Fruit Loops", "Stuttering", "Elephant toothpaste ", "A trash panda", "Jelly filled doughnut", "My personal Thanos glove", "Toenail fungus", "Bavarian cream", "Fuzzy bunny slippers", "My LEGO collection ", "Having knives for hands", "The sharpest tool in the shed", "A stack of 23 deflated bouncy houses", "Raging", "Double dipping", "Literally giving someone a hand", "The 95 theses", "Bacon", "Trees", "Planet", "Throwing yourself into a fireplace", "The Harlem Renaissance ", "WAFFLES!", "Thinking you are a salad so you pour dressing and lettuce on yourself ", "Breaking eggs all over yourself and saying,\u201dI am eggcellent!\u201d", "Pure nothingness", "Pollution", "Dancing zombies ", "Plastic water bottles", "Disappointing cinnamon rolls", "The Fire Nation", "Online school...", "50 kilograms of kinetic sand", "The attack", "Play Doh, lots of Play Doh", "87 cubic meters of chopped almonds", "Gangster Spongebob shirts from the early 2000's", "My therapist", "Projectile Vomit", "A poor Nigerian prince waiting for an email reply", "The Pentagon", "The 2 x 4's that I got at Home Depot", "A Barbershop Quartet", "Michael Jordan", "Breakfast cereals", "Danny DeVito", "Bubble Guppies", "_____, that was a mistake.", "Being wanted in every state but North Dakota, Idaho, and Illinois.", "The fourth reich", "Zits... EVERYWHERE!", "Your Mom", "Walking like an Egyptian", "Kool-Aid", "Ben Shapiro", "Diary of a Wimpy Kid", "I am a farmer", "Fart", "Barry Benson", "Grunting noises", "Egg Sandwich ", "The Kool-Aid Man", "Jefferson Davis", "A 3D jigsaw puzzle shaped mysteriously like a kidney ", "My dog", "My cat", "My gerbil", "My mid-life crisis"], "questions": ["I studied for 2 weeks straight for the AP [[BLANK]] exam only to get a 3. I might as well drop out.", "I can\u2019t send my son to school, he\u2019s allergic to [[BLANK]].", "My teacher promised me extra credit as long as I give him/her [[BLANK]]", "May I speak to the manager? I found [[BLANK]] in my salad.", "Don\u2019t ask me why, but I\u2019ve got [[BLANK]].", "Under the couch cushions I found [[BLANK]].", "I sprayed my cat with [[BLANK]].", "I've reached enlightenment, my secret is to [[BLANK]].", "My car crashed because of [[BLANK]].", "My water bottle is filled with [[BLANK]].", "What is this mysterious liquid?", "Imagine my surprise when [[BLANK]].", "Should I ask my parents for [[BLANK]]?", "Welcome to the new game show, \"Who wants to be a [[BLANK]]?\"", "Why I hate the school bathroom.", "Wearing shorts in winter is like [[BLANK]].", "The box was filled with [[BLANK]].", "My mailbox was stuffed with [[BLANK]].", "When I look down I see [[BLANK]].", "TV's favorite reality show, \"Chasing [[BLANK]].\"", "I went on vacation and I saw [[BLANK]].", "I cleaned out my attic and found [[BLANK]].", "Thinking of ideas for Ian\u2019s internet cards is like [[BLANK]].", "The Earth will explode in 24 hours because of [[BLANK]].", "At last, I have found my long lost [[BLANK]].", "I was walking down the street and ran into [[BLANK]].", "For Lent, I gave up [[BLANK]].", "My birthday consisted of [[BLANK]].", "I love [[BLANK]].", "When I die, I would like to be buried with [[BLANK]].", "What makes me laugh?", "My superpower is [[BLANK]].", "When I grow up, I want to be a [[BLANK]]!", "There are too many [[BLANK]].", "This thing is too fast for [[BLANK]].", "The greatest thing in life is [[BLANK]].", "[[BLANK]] is why I\u2019m dropping out of school.", "I was arrested because of [[BLANK]]. ", "During my morning meditation, I realized it would be a great idea to [[BLANK]].", "There\u2019s nothing better than a clown car filled with [[BLANK]].", "Drop your weapon and put your [[BLANK]] up!", "Once upon a time in [[BLANK]]", "It doesn\u2019t get any better than [[BLANK]] on a cold night.", "It\u2019s as if the [[BLANK]] was sucked out of me.", "I hate [[BLANK]].", "Africa is on fire because they found [[BLANK]].", "If Coronavirus doesn't kill me, [[BLANK]] will.", "Greek mythology and [[BLANK]] are my two favorite things.", "It will be the end of the world when [[BLANK]] happens.", "Why is my favorite chair wet?", "It burns when I [[BLANK]].", "[[BLANK]] roasting on an open fire.", "www.[[BLANK]].com", "I can\u2019t hold back any longer I have to [[BLANK]].", "A magician made [[BLANK]] blank appear.", "Ted Shecker\u2019s [[BLANK]] emporium.", "It was as if the earth opened up and [[BLANK]] me.", "My sweatshirt has [[BLANK]] stains.", "I don\u2019t want to eat any more [[BLANK]].", "Thanos had the infinity gauntlet, I have the infinity [[BLANK]].", "The best part of my day is [[BLANK]].", "They won\u2019t take me, I have [[BLANK]].", "When my toilet paper runs out I use [[BLANK]].", "What is that weird sound?", "A text book case of [[BLANK]].", "Super Mario's newest enemy, [[BLANK]].", "It's the newest Mario Kart item, [[BLANK]].", "What is that horrid smell?", "As supreme leader of Earth, my first order of business is to outlaw [[BLANK]].", "With my new time machine, I shall bring [[BLANK]] back from the past.", "My new invention creates [[BLANK]].", "The orcs are invading, launch the [[BLANK]]!", "Breaking news: historians have discovered that [[BLANK]] was the true cause of the Dark Ages.", "When I looked out the window I saw [[BLANK]].", "I [[BLANK]] to school every day.", "I use my phone for [[BLANK]].", "When I grow up I want to be [[BLANK]]!", "While sitting on the toilet I like to think about [[BLANK]].", "I opened my present but instead of something nice I got [[BLANK]].", "[[BLANK]] really rocks.", "Off in the distance, you can see a wild [[BLANK]] in its natural habitat.", "BREAKING NEWS: US Government announces that [[BLANK]] is now illegal nation-wide!", "And the last thing I said to her was: \u201cIt\u2019s either me or [[BLANK]].\u201d Now I\u2019m single.", "[[BLANK]] are like onions. They have layers.", "I\u2019m certain the reason I got into Harvard on a full ride was due to my essay about [[BLANK]]. ", "But everything changed when [[BLANK]] attacked.", "The United States is no more. What happened?", "What did the boomers leave us?", "I cannot stand [[BLANK]].", "The thing I fear most has to be [[BLANK]].", "Rome wasn't built in a day, but [[BLANK]] sure was.", "I, [[BLANK]], have a dream.", "If I was in Avatar, I would be a [[BLANK]] bender.", "To keep my mind sharp in social isolation, I have been [[BLANK]]."]}, "R": {"answers": ["The purple crayon I used as a dildo when I was homeless ", "Nut sock", "Your brother\u2019s homemade fleshlight ", "Party balloons and ribbed condoms", "Anime tities", "Self-mutalation", "My prolapsed anus", "Smegma", "Ice Age Baby", "The passion of Christ", "Cowboys getting fucked in their sleep", "Taking 31 Tylenol to numb the pain", "A nut that travels at the speed of sound", "Accidental circumcision", "A condom, a Wii remote, and a lot of lube", "One little, two little, three little penises, Four little, five little, six little penises, Seven little, eight little, nine little penises, Ten little penises", "The day that Pornhub Premium was free for everyone", "My 20 Terabyte homework folder", "Jared from Subway", "Wearing your boyfriends circumcised foreskin as a promise ring", "The fact that I know that it is not a beach, but it is a bathtub", "The fact that gen Z will be known as the generation that wanted to die mutually ", "Those 'play this and you will cum in 5 seconds' ads ", "Hentai", "1 man, 1 jar", "\"Fingerpainting\"", "2 girls, 1 cup", "A homemade fleshlight with two sponges and a rubber glove", "If you like PornHub Premium than you'll love _______.", "A pair of Switch joycons attached to my chode", "Sideboob", "Boob sweat", "Beastiality", "Shoving the skinny end of a screwdriver up my urethra", "Inflation, not the economic principle, but the fetish", "Ludwig van Beethoven's fucked up eardrums ", "The Buzz Lightyear x Jessie fanfic I made", "Sex Addiction ", "Unbirth", "The Velcro strap-on my girlfriend pegs me with ", "Vaginal Worms", "Dat Ass", "Rule 34", "Fucking poopy head"], "questions": ["Breaking news: A sex tape has surfaced where President Obama is seen having sex with [[BLANK]].", "What made my cousin horny?", "So I was sitting there, [[BLANK]] on my titties.", "Oh god, I think I have a [[BLANK]] fetish.", "My crew is big and it keeps getting bigger. That's cuz Jesus Christ is my [[BLANK]].", "Stepbro, stop [[BLANK]].", "Last night I got [[BLANK]] stuck in my rectum ", "[[BLANK]] really turns me on."]}} -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const isProduction = location.origin === "https://internetcards.ianmorrill.com" 2 | 3 | const $ = q => document.querySelector(q) 4 | 5 | let ws = new WebSocket(isProduction ? "wss://internetcards.ianmorrill.com/ws" : "ws://127.0.0.1:8765") 6 | 7 | function sendObj(obj) { 8 | ws.send(JSON.stringify(obj)) 9 | } 10 | 11 | function showScene(scene) { 12 | document.querySelectorAll(".scene").forEach(el => { 13 | if (el.getAttribute("data-scene") === scene) { 14 | el.setAttribute("data-active-scene", "") 15 | }else{ 16 | el.removeAttribute("data-active-scene") 17 | } 18 | }) 19 | } 20 | 21 | function setNavbar(left, right) { 22 | if (left) $("#nav-left").textContent = left 23 | $("#nav-right").textContent = right 24 | } 25 | 26 | function copyText(text) { 27 | let textbox = document.createElement("input") 28 | textbox.style.position = "fixed" 29 | textbox.style.top = "0" 30 | textbox.style.left = "0" 31 | textbox.style.opacity = "0" 32 | document.body.appendChild(textbox) 33 | 34 | textbox.value = text 35 | textbox.select() 36 | document.execCommand("copy") 37 | textbox.remove() 38 | } 39 | 40 | function addChatMessage(sender, content, color) { 41 | let msg = document.createElement("div") 42 | msg.title = "Sent " + (new Date()).toLocaleString() 43 | msg.className = "message" 44 | 45 | let senderText = document.createElement("b") 46 | senderText.textContent = sender + ": " 47 | senderText.style.color = color ? color : "#000000" 48 | 49 | let contentText = document.createTextNode(content) 50 | 51 | msg.appendChild(senderText) 52 | msg.appendChild(contentText) 53 | $("#message-list").appendChild(msg) 54 | 55 | $("#message-list").scrollTop = $("#message-list").scrollHeight - $("#message-list").clientHeight 56 | } 57 | 58 | function setUpPacks(packs) { 59 | return packs.map(pack => { 60 | let packLabel = document.createElement("label") 61 | packLabel.htmlFor = "select-pack-" + pack 62 | 63 | let packLabelText = document.createTextNode(pack) 64 | packLabel.appendChild(packLabelText) 65 | 66 | let packCheckbox = document.createElement("input") 67 | packCheckbox.type = "checkbox" 68 | packCheckbox.id = "select-pack-" + pack 69 | packLabel.appendChild(packCheckbox) 70 | 71 | $("#mr-packs").appendChild(packLabel) 72 | 73 | return {name: pack, checkbox: packCheckbox} 74 | }) 75 | } 76 | let packs = setUpPacks(["PG", "PG-13", "R"]) 77 | let customQuestions = [] 78 | let customAnswers = [] 79 | 80 | function updateCustomCardsEditor() { 81 | $("#ayoc-questions").textContent = "" 82 | $("#ayoc-answers").textContent = "" 83 | 84 | customQuestions.forEach((q, i) => { 85 | const questionRow = document.createElement("tr") 86 | 87 | const questionTextContainer = document.createElement("td") 88 | const questionText = document.createElement("input") 89 | questionText.value = q 90 | questionText.addEventListener("input", e => { 91 | customQuestions[i] = questionText.value 92 | }) 93 | questionTextContainer.appendChild(questionText) 94 | questionRow.appendChild(questionTextContainer) 95 | 96 | const questionRemoverContainer = document.createElement("td") 97 | const questionRemover = document.createElement("button") 98 | questionRemover.textContent = "Remove" 99 | questionRemover.className = "btn danger" 100 | questionRemover.addEventListener("click", e => { 101 | customQuestions.splice(i, 1) 102 | updateCustomCardsEditor() 103 | }) 104 | questionRemoverContainer.appendChild(questionRemover) 105 | questionRow.appendChild(questionRemoverContainer) 106 | 107 | $("#ayoc-questions").appendChild(questionRow) 108 | }) 109 | 110 | customAnswers.forEach((q, i) => { 111 | const answerRow = document.createElement("tr") 112 | 113 | const answerTextContainer = document.createElement("td") 114 | const answerText = document.createElement("input") 115 | answerText.value = q 116 | answerText.addEventListener("input", e => { 117 | customAnswers[i] = answerText.value 118 | }) 119 | answerTextContainer.appendChild(answerText) 120 | answerRow.appendChild(answerTextContainer) 121 | 122 | const answerRemoverContainer = document.createElement("td") 123 | const answerRemover = document.createElement("button") 124 | answerRemover.textContent = "Remove" 125 | answerRemover.className = "btn danger" 126 | answerRemover.addEventListener("click", e => { 127 | customAnswers.splice(i, 1) 128 | updateCustomCardsEditor() 129 | }) 130 | answerRemoverContainer.appendChild(answerRemover) 131 | answerRow.appendChild(answerRemoverContainer) 132 | 133 | $("#ayoc-answers").appendChild(answerRow) 134 | }) 135 | } 136 | 137 | let addCustomCardsButton = document.createElement("button") 138 | addCustomCardsButton.className = "btn" 139 | addCustomCardsButton.addEventListener("click", e => { 140 | updateCustomCardsEditor() 141 | showScene("add-your-own-cards") 142 | }) 143 | $("#mr-packs").appendChild(addCustomCardsButton) 144 | 145 | let score = 0 146 | 147 | $("#make-room").addEventListener("click", e => { 148 | customQuestions = [] 149 | customAnswers = [] 150 | addCustomCardsButton.textContent = "Add Your Own Cards..." 151 | showScene("make-room") 152 | }) 153 | 154 | $("#ayoc-add-question").addEventListener("click", e => { 155 | customQuestions.push("") 156 | updateCustomCardsEditor() 157 | }) 158 | 159 | $("#ayoc-add-answer").addEventListener("click", e => { 160 | customAnswers.push("") 161 | updateCustomCardsEditor() 162 | }) 163 | 164 | $("#ayoc-back").addEventListener("click", e => { 165 | if (customAnswers.length > 0 || customQuestions.length > 0) { 166 | addCustomCardsButton.textContent = "Edit Your Cards..." 167 | }else{ 168 | addCustomCardsButton.textContent = "Add Your Own Cards..." 169 | } 170 | showScene("make-room") 171 | }) 172 | 173 | $("#join-room").addEventListener("click", e => { 174 | showScene("join-room") 175 | }) 176 | 177 | $("#mr-back").addEventListener("click", e => { 178 | showScene("start") 179 | }) 180 | 181 | $("#jr-back").addEventListener("click", e => { 182 | showScene("start") 183 | }) 184 | 185 | $("#hl-back").addEventListener("click", e => { 186 | showScene("start") 187 | }) 188 | 189 | $("#rcl-cancel").addEventListener("click", e => { 190 | window.location.reload() 191 | }) 192 | 193 | $("#mr-submit").addEventListener("click", e => { 194 | const name = $("#mr-name").value 195 | const packsSelected = packs.map(({name, checkbox}) => { 196 | if (checkbox.checked) { 197 | return name 198 | } 199 | }).filter(pack => !!pack) 200 | 201 | if (name.trim() !== "") { 202 | sendObj({ 203 | action: "makeRoom", 204 | name, 205 | packs: packsSelected, 206 | questions: customQuestions.map(q => q.replace(/_+/, "[[BLANK]]")), 207 | answers: customAnswers 208 | }) 209 | } 210 | }) 211 | 212 | $("#jr-submit").addEventListener("click", e => { 213 | const name = $("#jr-name").value 214 | const joinCode = $("#jr-code").value 215 | 216 | if (name.trim() !== "") { 217 | if (joinCode.length === 6 && /[0-9]+/.test(joinCode)) { 218 | sendObj({action: "joinRoom", name, joinCode}) 219 | } 220 | } 221 | }) 222 | 223 | $("#rcl-start-game").addEventListener("click", e => { 224 | sendObj({action: "startGame"}) 225 | }) 226 | 227 | $("#open-chat").addEventListener("click", e => { 228 | $(".bottom-bar").classList.add("hidden") 229 | $(".content").classList.remove("sidebar-hidden") 230 | }) 231 | 232 | $("#close-chat").addEventListener("click", e => { 233 | $(".bottom-bar").classList.remove("hidden") 234 | $(".content").classList.add("sidebar-hidden") 235 | }) 236 | 237 | $("#msg-form").addEventListener("submit", e => { 238 | e.preventDefault() 239 | const content = $("#msg-content").value 240 | 241 | if (content.trim() !== "") { 242 | sendObj({action: "sendChat", content}) 243 | addChatMessage("You", content, "#0000ff") 244 | $("#msg-content").value = "" 245 | } 246 | }) 247 | 248 | ws.addEventListener("open", () => { 249 | if (/#j\-([0-9]{6})/.test(location.hash)) { 250 | $("#jr-code").value = location.hash.match(/#j\-([0-9]{6})/)[1] 251 | location.hash = "" 252 | showScene("join-room") 253 | }else{ 254 | showScene("start") 255 | } 256 | }) 257 | 258 | ws.addEventListener("close", () => { 259 | showScene("closed") 260 | }) 261 | 262 | ws.addEventListener("message", e => { 263 | const msgObj = JSON.parse(e.data) 264 | console.log(msgObj) 265 | if (msgObj.action === "roomMade") { 266 | score = 0 267 | setNavbar(msgObj.setName, "Room #" + msgObj.joinCode) 268 | 269 | $("#rcl-code").textContent = msgObj.joinCode 270 | $("#rcl-members").textContent = "" 271 | 272 | $("#rcl-copy-invite").addEventListener("click", () => { 273 | copyText(location.origin + location.pathname + "#j-" + msgObj.joinCode) 274 | }) 275 | 276 | showScene("room-creator-lobby") 277 | }else if (msgObj.action === "roomJoined") { 278 | score = 0 279 | setNavbar(msgObj.setName, "Room #" + msgObj.joinCode) 280 | 281 | $("#rjl-members").textContent = "" 282 | msgObj.members.forEach(m => { 283 | const memberItem = document.createElement("li") 284 | memberItem.textContent = m 285 | $("#rjl-members").appendChild(memberItem) 286 | }) 287 | showScene("room-joiner-lobby") 288 | }else if (msgObj.action === "memberJoined") { 289 | const rjlMemberItem = document.createElement("li") 290 | rjlMemberItem.textContent = msgObj.name 291 | $("#rjl-members").appendChild(rjlMemberItem) 292 | 293 | const rclMemberItem = document.createElement("li") 294 | rclMemberItem.textContent = msgObj.name 295 | $("#rcl-members").appendChild(rclMemberItem) 296 | }else if (msgObj.action === "memberLeft") { 297 | ["#rcl-members", "#rjl-members"].forEach(q => { 298 | $(q).childNodes[msgObj.id].remove() 299 | }) 300 | }else if (msgObj.action === "gameStart") { 301 | $(".bottom-bar").classList.remove("hidden") 302 | }else if (msgObj.action === "newQuestion") { 303 | setNavbar(null, score + " Point" + (score == 1 ? "" : "s")) 304 | $("#q-question").textContent = msgObj.question 305 | $("#q-answers").textContent = "" 306 | $("#q-judge").textContent = msgObj.judge 307 | msgObj.hand.forEach(a => { 308 | const answerContent = document.createElement("div") 309 | answerContent.textContent = a 310 | 311 | const answerButton = document.createElement("button") 312 | answerButton.appendChild(answerContent) 313 | answerButton.addEventListener("click", () => { 314 | sendObj({action: "answerSubmission", answer: a}) 315 | $("#wfo-question").textContent = msgObj.question 316 | $("#wfo-answer").textContent = a 317 | showScene("waiting-for-others") 318 | }) 319 | 320 | $("#q-answers").appendChild(answerButton) 321 | }) 322 | showScene("question") 323 | }else if (msgObj.action === "showAnswers") { 324 | $("#wfj-question").textContent = msgObj.question 325 | $("#wfj-answers").textContent = "" 326 | $("#wfj-judge").textContent = msgObj.judge 327 | msgObj.answers.forEach(a => { 328 | const answerItem = document.createElement("div") 329 | 330 | const answerCard = document.createElement("div") 331 | answerCard.classList.add("card") 332 | answerCard.classList.add("red") 333 | answerCard.textContent = a 334 | 335 | answerItem.appendChild(answerCard) 336 | 337 | $("#wfj-answers").appendChild(answerItem) 338 | }) 339 | showScene("waiting-for-judge") 340 | }else if (msgObj.action === "selectAnswer") { 341 | $("#j-question").textContent = msgObj.question 342 | $("#j-answers").textContent = "" 343 | msgObj.answers.forEach(a => { 344 | const answerContent = document.createElement("div") 345 | answerContent.textContent = a 346 | 347 | const answerButton = document.createElement("button") 348 | answerButton.appendChild(answerContent) 349 | answerButton.addEventListener("click", () => { 350 | sendObj({action: "selectedAnswer", answer: a}) 351 | }) 352 | 353 | $("#j-answers").appendChild(answerButton) 354 | }) 355 | showScene("judging") 356 | }else if (msgObj.action === "chatMessage") { 357 | addChatMessage(msgObj.from, msgObj.content, "#000000") 358 | }else if (msgObj.action === "updateScore") { 359 | score = msgObj.score 360 | setNavbar(null, score + " Point" + (score == 1 ? "" : "s")) 361 | }else if (msgObj.action === "judgeSelected") { 362 | $("#js-question").textContent = msgObj.question 363 | $("#js-answer").textContent = msgObj.answer 364 | $("#js-submitted-by").textContent = msgObj.submittedBy 365 | showScene("judge-selected") 366 | }else if (msgObj.action === "youAreTheJudge") { 367 | $("#yatj-question").textContent = msgObj.question 368 | showScene("you-are-the-judge") 369 | }else if (msgObj.action === "hostLeft") { 370 | setNavbar("Ian's Internet Cards", "") 371 | showScene("host-left") 372 | }else if (msgObj.action === "preparingGame") { 373 | showScene("preparing-game") 374 | }else if (msgObj.action === "error") { 375 | alert(msgObj.content) 376 | } 377 | }) --------------------------------------------------------------------------------