├── app
├── process
│ └── .gitkeep
├── data
│ ├── players
│ │ └── .gitkeep
│ ├── questions
│ │ └── custom
│ │ │ ├── .gitkeep
│ │ │ └── media
│ │ │ └── .gitkeep
│ ├── fonts
│ │ ├── Values.ttf
│ │ ├── Category.otf
│ │ └── Question.otf
│ └── audio
│ │ ├── Time Out.mp3
│ │ ├── Daily Double.mp3
│ │ ├── Question Open.mp3
│ │ └── Final Jeopardy.mp3
├── src
│ ├── JConstants.java
│ ├── Player.java
│ ├── Media.java
│ ├── ScrollableScreen.java
│ ├── Category.java
│ ├── Round.java
│ ├── Console.java
│ ├── Question.java
│ └── Game.java
└── scripts
│ ├── customs.py
│ └── scraper.py
├── .gitattributes
├── media
├── board.png
├── setup.png
├── console.png
├── question.png
└── custom.json
├── docs
├── data
│ ├── Time Out.mp3
│ ├── Round Over.mp3
│ ├── Daily Double.mp3
│ ├── Question Open.mp3
│ └── Final Jeopardy.mp3
├── console.html
├── index.html
├── style.css
├── console.js
├── lib
│ └── broadcast-channel.min.js
└── client.js
├── .gitignore
└── README.md
/app/process/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/data/players/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/data/questions/custom/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/data/questions/custom/media/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | docs/* linguist-documentation=false
2 |
--------------------------------------------------------------------------------
/media/board.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/media/board.png
--------------------------------------------------------------------------------
/media/setup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/media/setup.png
--------------------------------------------------------------------------------
/media/console.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/media/console.png
--------------------------------------------------------------------------------
/media/question.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/media/question.png
--------------------------------------------------------------------------------
/docs/data/Time Out.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/docs/data/Time Out.mp3
--------------------------------------------------------------------------------
/app/data/fonts/Values.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/app/data/fonts/Values.ttf
--------------------------------------------------------------------------------
/docs/data/Round Over.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/docs/data/Round Over.mp3
--------------------------------------------------------------------------------
/app/data/audio/Time Out.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/app/data/audio/Time Out.mp3
--------------------------------------------------------------------------------
/app/data/fonts/Category.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/app/data/fonts/Category.otf
--------------------------------------------------------------------------------
/app/data/fonts/Question.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/app/data/fonts/Question.otf
--------------------------------------------------------------------------------
/docs/data/Daily Double.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/docs/data/Daily Double.mp3
--------------------------------------------------------------------------------
/docs/data/Question Open.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/docs/data/Question Open.mp3
--------------------------------------------------------------------------------
/docs/data/Final Jeopardy.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/docs/data/Final Jeopardy.mp3
--------------------------------------------------------------------------------
/app/data/audio/Daily Double.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/app/data/audio/Daily Double.mp3
--------------------------------------------------------------------------------
/app/data/audio/Question Open.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/app/data/audio/Question Open.mp3
--------------------------------------------------------------------------------
/app/data/audio/Final Jeopardy.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nathansbud/Jeopardizer/HEAD/app/data/audio/Final Jeopardy.mp3
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | out/
3 | *.iml
4 |
5 | app/data/META-INF/MANIFEST.MF
6 | app/data/players/data.txt
7 |
8 | docs/custom.json
9 |
--------------------------------------------------------------------------------
/app/src/JConstants.java:
--------------------------------------------------------------------------------
1 | public final class JConstants {
2 | public static final String JEOPARDY_BLUE = "ff051281";
3 | public static final String JEOPARDY_YELLOW = "fff9ad46";
4 | public static final String JEOPARDY_WAGERABLE = "ffff0000";
5 | public static final String[] MONTHS = {
6 | "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/Player.java:
--------------------------------------------------------------------------------
1 | public class Player {
2 | private static Player active = null;
3 | private String name;
4 |
5 | private int score;
6 | private int wins;
7 |
8 | public Player(String _name) {
9 | name = _name;
10 | }
11 | public Player(String _name, int _wins) {
12 | name = _name;
13 | wins = _wins;
14 | }
15 |
16 | public String getName() {
17 | return name.split(" ")[0];
18 | }
19 | public String getFullName() {
20 | return name;
21 | }
22 | public void setName(String _name) {
23 | name = _name;
24 | }
25 |
26 | public int getScore() {
27 | return score;
28 | }
29 | public void changeScore(int amount) {
30 | score += amount;
31 | }
32 | public void setScore(int _score) {
33 | score = _score;
34 | }
35 |
36 |
37 | public boolean isActive() {
38 | return active != null && active.equals(this);
39 | }
40 | public static Player getActive() {
41 | return active;
42 | }
43 | public static void setActive(Player _active) {
44 | active = _active;
45 | }
46 |
47 | public int getWins() {
48 | return wins;
49 | }
50 | public void setWins(int _wins) {
51 | wins = _wins;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/Media.java:
--------------------------------------------------------------------------------
1 | public class Media {
2 | public enum MediaType {
3 | AUDIO(), //Extends minim
4 | VIDEO(), //Extends PVideo
5 | IMAGE() //Extends PImage
6 | }
7 |
8 | private MediaType type;
9 | private String name;
10 | private String path;
11 |
12 | private Object media;
13 |
14 | public Media(MediaType _type, String _path) {
15 | type = _type;
16 | path = _path;
17 | }
18 |
19 | public Media() {
20 |
21 | }
22 |
23 | public void load() {
24 | switch(type) {
25 | case AUDIO:
26 | media = Game.getMinim().loadFile(path);
27 | break;
28 | case VIDEO:
29 | break;
30 | case IMAGE:
31 | media = Game.getGUI().loadImage(path);
32 | break;
33 | default:
34 | break;
35 | }
36 | }
37 |
38 | public void load(String _path) {
39 | path = _path;
40 | load();
41 | }
42 |
43 | public Object getMedia() {
44 | return media;
45 | }
46 | public void setMedia(Object _media) {
47 | media = _media;
48 | }
49 |
50 | public MediaType getType() {
51 | return type;
52 | }
53 | public void setType(MediaType _type) {
54 | type = _type;
55 | }
56 |
57 | public String getName() {
58 | return name;
59 | }
60 | public void setName(String _name) {
61 | name = _name;
62 | }
63 |
64 | public String getPath() {
65 | return path;
66 | }
67 | public void setPath(String _path) {
68 | path = _path;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/ScrollableScreen.java:
--------------------------------------------------------------------------------
1 | import processing.core.PApplet;
2 |
3 | public class ScrollableScreen {
4 | static PApplet gui;
5 |
6 | private float width;
7 | private float height;
8 |
9 | private float maxWidth;
10 | private float maxHeight = 1600;
11 |
12 | private float viewX = 0;
13 | private float viewY = 0;
14 |
15 | public void draw() {
16 | gui.fill(PApplet.unhex(JConstants.JEOPARDY_BLUE));
17 | gui.rect(0, 0, gui.width, gui.height);
18 | gui.fill(PApplet.unhex(JConstants.JEOPARDY_YELLOW));
19 | }
20 |
21 | public static PApplet getGui() {
22 | return gui;
23 | }
24 | public static void setGui(PApplet _gui) {
25 | gui = _gui;
26 | }
27 |
28 | public float getWidth() {
29 | return width;
30 | }
31 |
32 | public void setWidth(float _width) {
33 | width = _width;
34 | }
35 |
36 | public float getHeight() {
37 | return height;
38 | }
39 |
40 | public void setHeight(float _height) {
41 | height = _height;
42 | }
43 |
44 | public float getMaxWidth() {
45 | return maxWidth;
46 | }
47 |
48 | public void setMaxWidth(float _maxWidth) {
49 | maxWidth = _maxWidth;
50 | }
51 |
52 | public float getMaxHeight() {
53 | return maxHeight;
54 | }
55 |
56 | public void setMaxHeight(float _maxHeight) {
57 | maxHeight = _maxHeight;
58 | }
59 |
60 | public float getViewX() {
61 | return viewX;
62 | }
63 | public void setViewX(float _viewX) {
64 | viewX = _viewX;
65 | }
66 |
67 | public float getViewY() {
68 | return viewY;
69 | }
70 |
71 | public void changeViewY(float _amt) {
72 | if(viewY + _amt <= maxHeight && viewY + _amt >= 0) viewY += _amt;
73 | }
74 |
75 | public void setViewY(float _viewY) {
76 | viewY = _viewY;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/scripts/customs.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3.7
2 |
3 | import os
4 | import json
5 |
6 | def make_customs(overwrite, question_count):
7 | path = os.path.join(os.path.dirname(__file__), "..", "process")
8 | custom_path = os.path.join(os.path.dirname(__file__), "..", "questions", "custom")
9 |
10 | for file in os.listdir(path):
11 | with open(path + file, "r") as custom:
12 | categories = custom.readlines()
13 | json_add = []
14 | for category in categories:
15 | cat = [qa.strip() for qa in category.split("|")]
16 | questions = cat[1::2]
17 | answers = cat[2::2]
18 |
19 | if len(questions) == len(answers) == question_count:
20 | qa_set = []
21 | for i in range(len(questions)):
22 | qa_set.append(
23 | {
24 | "Question":questions[i],
25 | "Answer":answers[i]
26 | }
27 | )
28 |
29 | json_add.append(
30 | {
31 | "Category":cat[0],
32 | "Clues":qa_set
33 | }
34 | )
35 | else:
36 | print("Excluded category due to incorrect question count: ")
37 | print(cat)
38 |
39 | exists = os.path.isfile(custom_path + file[:file.rfind(".")] + ".json")
40 | if not exists or overwrite:
41 | print("Made category file from " + file)
42 | with open(custom_path + file[:file.rfind(".")] + ".json", "w+") as custom_json:
43 | json.dump(json_add, custom_json)
44 | else:
45 | print("File already exists, did not overwrite!")
46 |
47 | if __name__ == "__main__":
48 | make_customs(True, 5)
49 | pass
50 |
--------------------------------------------------------------------------------
/docs/console.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Console - Jeopardizer
7 |
8 |
9 |
10 |
11 |
42 |
43 |
44 |
45 |
46 |
47 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
DAILY DOUBLE
60 |
61 | Amount:
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
Waiting for new game...
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Client - Jeopardizer
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Welcome to Jeopardizer!
14 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
DAILY DOUBLE
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
Final Scores:
77 |
80 |
81 |
82 |
83 |
Linked console was closed!
84 |
85 |
86 |
87 |
Sorry, your browser won't work with this website :(((
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/app/src/Category.java:
--------------------------------------------------------------------------------
1 | import java.util.ArrayList;
2 | import processing.core.PApplet;
3 |
4 | public class Category {
5 | private static PApplet gui;
6 | private String name;
7 | private String dialogue = "";
8 | private String date = "";
9 |
10 | private ArrayList questions = new ArrayList();
11 |
12 | private float x, y = 0;
13 | private float width, height;
14 |
15 | public Category() {
16 |
17 | }
18 |
19 | public void draw() {
20 | if(gui.args[0].equals("Game")) {
21 | if(Game.getCategoryFont() != null) {
22 | gui.textFont(Game.getCategoryFont());
23 | }
24 | gui.fill(PApplet.unhex(JConstants.JEOPARDY_BLUE));
25 | gui.rect(x, y, Question.getWidth(), Question.getHeight());
26 | gui.fill(255);
27 | gui.textSize(18);
28 | gui.text(name.trim(), x + 0.1f * Question.getWidth(), y + Question.getHeight() / 3.0f, Question.getWidth() * 0.9f, Question.getHeight());
29 | for (Question q : questions) {
30 | q.draw();
31 | }
32 | }
33 | }
34 |
35 | public void setValues(Round.RoundType r) {
36 | for(int i = 0; i < questions.size(); i++) {
37 | switch(r) {
38 | case SINGLE:
39 | questions.get(i).setValue(200+200*i);
40 | break;
41 | case DOUBLE:
42 | questions.get(i).setValue(400+400*i);
43 | break;
44 | case FINAL:
45 | questions.get(i).setValue(0);
46 | break;
47 | }
48 | }
49 | }
50 |
51 | public void setX(float _x) {
52 | x = _x;
53 | for(Question q : questions) {
54 | q.setX(x);
55 | }
56 | }
57 |
58 | public void setY(float _y) {
59 | y = _y;
60 | for(int i = 1; i < questions.size()+1; i++) {
61 | questions.get(i-1).setY(i*Question.getHeight()+(i)*Question.getHeightBuffer());
62 | }
63 | }
64 |
65 | public void setWidth(float _width) {
66 | width = _width;
67 | }
68 |
69 | public void setHeight(float _height) {
70 | height = _height;
71 | }
72 |
73 | public ArrayList getQuestions() {
74 | return questions;
75 | }
76 | public void setQuestions(ArrayList _questions) {
77 | questions = _questions;
78 | }
79 |
80 | public Question getQuestion(int index) {
81 | return questions.get(index);
82 | }
83 | public void addQuestion(Question q) {
84 | q.setCategory(name);
85 | questions.add(q);
86 | }
87 |
88 | public String getName() {
89 | return name;
90 | }
91 | public void setName(String _name) {
92 | name = _name;
93 | }
94 |
95 | public String getDialogue() {
96 | return dialogue;
97 | }
98 | public boolean hasDialogue() {
99 | return !dialogue.equals("");
100 | }
101 | public void setDialogue(String _dialogue) {
102 | dialogue = _dialogue;
103 | }
104 |
105 | public void setDate(String _date) {
106 | date = _date;
107 | for(Question q : questions) {
108 | q.setDate(_date);
109 | }
110 | }
111 | public String getDate() {
112 | return date;
113 | }
114 |
115 | public int getDay() {
116 | return date.length() > 0 ? Integer.parseInt(date.substring(date.indexOf("/")+1, date.lastIndexOf("/"))) : -1;
117 | }
118 | public int getMonth() {
119 | return date.length() > 0 ? Integer.parseInt(date.substring(0, date.indexOf("/"))) : -1;
120 | }
121 | public int getYear() {
122 | return date.length() > 0 ? Integer.parseInt(date.substring(date.lastIndexOf("/")+1)) : -1;
123 | }
124 | public String getMonthName() {
125 | return date.length() > 0 ? JConstants.MONTHS[getMonth()-1] : "";
126 | }
127 |
128 |
129 | public static void setGui(PApplet _gui) {
130 | gui = _gui;
131 | }
132 | }
133 |
134 |
--------------------------------------------------------------------------------
/docs/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --jeopardy-blue: rgb(5, 20, 129);
3 | --jeopardy-gold: rgb(226, 176, 114);
4 | --jeopardy-red: #dc143c;
5 | }
6 |
7 | /* Safari doesn't pick up CSS rules on body, for some reason? */
8 | .game-container {
9 | background: var(--jeopardy-blue);
10 | color: var(--jeopardy-gold);
11 | text-align: center;
12 | font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
13 | overflow: hidden;
14 | }
15 |
16 | body, body:fullscreen {
17 | background: var(--jeopardy-blue);
18 | color: var(--jeopardy-gold);
19 | text-align: center;
20 | font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
21 | overflow: hidden;
22 | }
23 |
24 | body, #game-container, div, .game_table {background: var(--jeopardy-blue);}
25 |
26 | div:not(#start):not(.game-container):not(.show), #final_text, .footnote_text, #daily_double, button[data-manual='false'], [data-manual='support'], .hide {
27 | display: none;
28 | }
29 |
30 | button:hover, input[type='submit'] {
31 | cursor: pointer;
32 | }
33 |
34 | nav {
35 | width: 100vw;
36 | }
37 | nav ul {list-style-type: none;}
38 | nav li {
39 | text-decoration: none;
40 | color: var(--jeopardy-gold);
41 | }
42 |
43 | #player_list {
44 | display: table;
45 | margin: 0 auto;
46 | padding: 5px;
47 | }
48 |
49 | #player_list li {
50 | text-align: left;
51 | }
52 |
53 | #player_controls {
54 | margin-top: 5px;
55 | }
56 | #player_list li button, #player_list li span, #timer * {
57 | margin-right: 5px;
58 | }
59 |
60 | #scores {
61 | font-size: 300%;
62 | }
63 |
64 | form {
65 | display: table;
66 | margin-left: auto;
67 | margin-right: auto;
68 | text-align: left;
69 | }
70 | form input, form label {margin: 5px;}
71 | form input:not([type='number']):not([type='text']):not([type='submit']) {
72 | margin: 15px;
73 | }
74 |
75 | p {display: table-row;}
76 | form input {display: table-cell;}
77 | form label {
78 | display: table-cell;
79 | text-align: right;
80 | }
81 |
82 | #custom_label {
83 | text-decoration: underline;
84 | color: lightblue;
85 | }
86 | #custom_label::before {
87 | content: "[";
88 | color: var(--jeopardy-gold);
89 | }
90 | #custom_label::after {
91 | content: "]";
92 | color: var(--jeopardy-gold);
93 | }
94 | #custom_label:hover {cursor: pointer;}
95 | #player_scores {list-style-type: none;}
96 |
97 | .game_table, .game_table td, .game_table th {
98 | background: var(--jeopardy-blue);
99 | table-layout: fixed;
100 | border: 6px solid black;
101 | border-collapse: collapse;
102 | }
103 | .game_table th {
104 | color: white;
105 | font-size: 125%;
106 | }
107 | .game_table td {
108 | color: var(--jeopardy-gold);
109 | font-size: 200%;
110 | }
111 |
112 |
113 | .game_table:not(.console) {
114 | position: absolute;
115 | top: 0;
116 | bottom: 0;
117 | left: 0;
118 | right: 0;
119 | width: 100%;
120 | height: 100%;
121 | font-weight: bold;
122 | }
123 |
124 | .comments * {
125 | text-align: left;
126 | }
127 |
128 | #daily_double {color: var(--jeopardy-red);}
129 | #question {
130 | padding-top: 25vh;
131 | padding-bottom: 25vh;
132 | color: white;
133 | }
134 | #question * {margin: 10px;}
135 | .question_cell {cursor: grab;}
136 | .game_table.dd_enabled .question_cell[data-dd='true']:not([data-client='true']) {
137 | color: var(--jeopardy-red);
138 | }
139 |
140 | .question_cell.console.seen {color: #60523E;}
141 | .game_table.dd_enabled .question_cell.console.seen[data-dd='true'] {color: #551111;}
142 |
143 |
144 | .question_cell[disabled='true'] {
145 | cursor: default;
146 | color: var(--jeopardy-blue);
147 | font-size: 0;
148 | }
149 |
150 | .game_table.console {
151 | width: 100%;
152 | }
153 |
154 | .footnote {color: lightblue;}
155 | .footnote:hover {cursor: pointer;}
156 |
157 | .center_container {
158 | display: flex;
159 | align-items: center;
160 | justify-content: center;
161 | }
162 | .center_container button {
163 | margin: 5px;
164 | }
165 |
166 | #media {
167 | width: 100vw;
168 | height: 100vh;
169 | }
170 |
171 | img, video {
172 | border: 2px solid white;
173 | position: relative;
174 | top: 50%;
175 | transform: translateY(-50%);
176 | max-height: 800;
177 | max-width: 800;
178 | }
179 |
--------------------------------------------------------------------------------
/app/src/Round.java:
--------------------------------------------------------------------------------
1 | import java.util.ArrayList;
2 | import java.util.concurrent.ThreadLocalRandom;
3 |
4 | public class Round {
5 | enum RoundType {
6 | SINGLE(),
7 | DOUBLE(),
8 | FINAL(),
9 |
10 | CUSTOM(),
11 | }
12 |
13 | private static Round CURRENT_ROUND = null;
14 |
15 | private RoundType round;
16 | private ArrayList categories = new ArrayList();
17 |
18 | private int filterYear = -1;
19 | private boolean filter;
20 |
21 | public Round(RoundType _round) {
22 | round = _round;
23 | }
24 |
25 | public RoundType getRoundType() {
26 | return round;
27 | }
28 | public void setRoundType(RoundType _round) {
29 | round = _round;
30 | }
31 |
32 | public void setup() {
33 | if(round != RoundType.FINAL) {
34 | for (int i = 0; i < categories.size(); i++) {
35 | categories.get(i).setX(i * Question.getWidth() + i * Question.getWidthBuffer());
36 | categories.get(i).setY(0);
37 | categories.get(i).setValues(round);
38 | for(Question q : categories.get(i).getQuestions()) {
39 | if(categories.get(i).hasDialogue()) {
40 | q.setDialogue(categories.get(i).getDialogue());
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
47 | public boolean hasFilter() {
48 | return filter;
49 | }
50 | public void setFilter(boolean _filter) {
51 | filter = _filter;
52 | }
53 |
54 | public int getFilterYear() {
55 | return filterYear;
56 | }
57 | public void setFilterYear(int _filterYear) {
58 | setFilter(true);
59 | filterYear = _filterYear;
60 | }
61 |
62 | public int getNumAnswered() {
63 | int num = 0;
64 | for(Category c : categories) {
65 | for(Question q : c.getQuestions()) {
66 | if(q.isAnswered()) {
67 | num++;
68 | }
69 | }
70 | }
71 | return num;
72 | }
73 | public int getQuestionCount() {
74 | int count = 0;
75 | for(Category c : categories) {
76 | count += c.getQuestions().size();
77 | }
78 | return count;
79 | }
80 |
81 | public void draw() {
82 | if(Question.getSelected() == null) {
83 | for (Category c : categories) {
84 | c.draw();
85 | }
86 | } else {
87 | Question.getSelected().draw();
88 | }
89 | }
90 |
91 | public ArrayList getCategories() {
92 | return categories;
93 | }
94 | public Category getCategory(int index) {
95 | return categories.get(index);
96 | }
97 |
98 | public void setWagerables() {
99 | int rand = ThreadLocalRandom.current().nextInt(0, 30);
100 |
101 | switch(round) {
102 | case SINGLE:
103 | if(this.getQuestionCount() == 30) {
104 | categories.get(rand/6).getQuestions().get(rand%5).setDailyDouble(true);
105 | }
106 | break;
107 | case DOUBLE:
108 | if(this.getQuestionCount() == 30) {
109 | categories.get(rand / 6).getQuestions().get(rand % 5).setDailyDouble(true);
110 | int randD = ThreadLocalRandom.current().nextInt(0, 30);
111 | while (randD == rand) {
112 | randD = ThreadLocalRandom.current().nextInt(0, 30);
113 | }
114 | categories.get(randD / 6).getQuestions().get(randD % 5).setDailyDouble(true);
115 | }
116 | break;
117 | case FINAL:
118 | categories.get(0).getQuestions().get(0).setWagerable(true);
119 | break;
120 | }
121 | }
122 |
123 | public void addCategory(Category c) {
124 | categories.add(c);
125 | }
126 |
127 | public static void setCurrentRound(Round _currentRound) {
128 | CURRENT_ROUND = _currentRound;
129 | if(CURRENT_ROUND.getRoundType() == RoundType.FINAL) {
130 | CURRENT_ROUND.getCategories().get(0).getQuestions().get(0).setAnswered(true);
131 | Question.setSelected(CURRENT_ROUND.getCategories().get(0).getQuestions().get(0));
132 | }
133 | }
134 | public static Round getCurrentRound() {
135 | return CURRENT_ROUND;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/app/src/Console.java:
--------------------------------------------------------------------------------
1 | import processing.core.PApplet;
2 |
3 | public class Console extends PApplet {
4 | @Override public void settings() {
5 | fullScreen(1);
6 | }
7 |
8 | @Override public void draw() {
9 | background(PApplet.unhex(JConstants.JEOPARDY_BLUE));
10 | textSize(20);
11 |
12 |
13 | for (int i = 0; i < Game.getPlayers().size(); i++) {
14 | if (Game.getPlayers().get(i).isActive()) {
15 | fill(PApplet.unhex(JConstants.JEOPARDY_YELLOW));
16 | } else {
17 | fill(255);
18 | }
19 | text(Game.getPlayers().get(i).getName() + ": $" + (Game.getPlayers().get(i).getScore()), width / 10.0f + width / 8.0f * (i), height / 18.0f);
20 | }
21 | textSize(40);
22 |
23 | try {
24 | if(Question.getSelected() != null) {
25 | if (Question.getSelected().isWagerable()) {
26 | fill(PApplet.unhex(JConstants.JEOPARDY_WAGERABLE));
27 | if (Question.getSelected().isDailyDouble()) {
28 | text("DAILY DOUBLE", width / 2.0f - 0.5f * textWidth("DAILY DOUBLE"), height / 10.0f);
29 | } else {
30 | text("FINAL JEOPARDY", width / 2.0f - 0.5f * textWidth("FINAL JEOPARDY"), height / 10.0f);
31 | }
32 |
33 | text(Game.getWager(), width / 2.0f - 0.5f * textWidth(Game.getWager()), height / 8.0f);
34 | }
35 |
36 | if(Game.getTimerState()) {
37 | fill(PApplet.unhex(JConstants.JEOPARDY_WAGERABLE));
38 | } else {
39 | fill(255);
40 | }
41 |
42 | text((Game.getTimerState()) ? ("(Timer Running)") : ("(Timer Stopped)"), width/2.0f - 0.5f * textWidth("(Timer Running)"), 0 + height/4.0f);
43 | if(Question.getSelected().hasMedia() && Question.getSelected().getMedia().getType() != Media.MediaType.AUDIO) {
44 | text((Question.getSelected().isShowMedia()) ? ("(Showing Media: ") : ("(Unshown Media: ") + Question.getSelected().getMedia().getName() + ")", width/2.0f - 0.5f * textWidth("(Unshown Media: " + Question.getSelected().getMedia().getName() + ")"), height/4.0f + height/20.0f);
45 | }
46 | fill(255);
47 | if(Question.getSelected().getCategory() != null && !Question.getSelected().getCategory().equals("")) {
48 | text(Question.getSelected().getCategory(), width / 2.0f - 0.5f * textWidth(Question.getSelected().getCategory()), 0 + height / 5.0f);
49 | }
50 | if(Question.getSelected().getQuestion() != null && !Question.getSelected().getQuestion().equals("")) {
51 | text(Question.getSelected().getQuestion(), width / 8.0f, height / 3.0f, width - width / 3.0f, height);
52 | }
53 | if(Question.getSelected().getAnswer() != null && !Question.getSelected().getAnswer().equals("")) {
54 | text(Question.getSelected().getAnswer(), width / 8.0f, height - height / 5.0f);
55 | }
56 | if(Question.getSelected().getDate() != null && !Question.getSelected().getDate().equals("")) {
57 | text(Question.getSelected().getDate(), width - 2 * textWidth(Question.getSelected().getDate()), height - height / 5.0f);
58 | }
59 | } else {
60 | textSize(25);
61 | fill(255);
62 |
63 | int offsetDialogue = 0;
64 | int offsetDailyDouble = 0;
65 |
66 | for(int i = 0; i < Round.getCurrentRound().getCategories().size(); i++) {
67 | Category c = Round.getCurrentRound().getCategory(i);
68 | if(c.hasDialogue()) {
69 | offsetDialogue++;
70 | text(c.getDialogue().trim(), width/1.95f, height/4.0f*(offsetDialogue), width/3.0f, height);
71 | }
72 | for(int j = 0; j < c.getQuestions().size(); j++) {
73 | if(c.getQuestions().get(j).isDailyDouble()) {
74 | offsetDailyDouble++;
75 | text("DD: " + c.getName() + " (" + c.getQuestions().get(j).getValue() + ")", width/144.0f, height/4.0f*offsetDailyDouble, width/2.0f, height);
76 | }
77 | }
78 | }
79 | }
80 | } catch(NullPointerException e) {
81 | for(Object o : e.getStackTrace()) {
82 | System.out.println(o);
83 | }
84 | System.out.println("Encountered NullPointerException in Console, despite null checks");
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Jeopardizer: Jeopardy, for the folks at home!
2 |
3 | 
4 |
5 | Jeopardizer is a webapp to host local games of Jeopardy! Designed for two monitors, one player takes the role of host, and the remaining players try their hands as contestants.
6 |
7 | Games are loaded from [J-Archive](https://j-archive.com/) by providing the ID of a specific game (seen [here](https://j-archive.com/showgame.php?game_id=6969) as a URL parameter).
8 |
9 | ## Custom Games
10 |
11 | By default, Jeopardizer pulls a provided J-Archive game. However, custom games can be defined as JSON files, and loaded in during game setup by the host. A [base template](./media/custom.json) is provided. Rounds can have any number of categories, categories can have any number of questions, and round board size will always be `# Categories * Max(# Questions)` for consistency.
12 |
13 | Question values can either be defined via a round multiplier (e.g. 2 for Double Jeopardy scoring), or on questions themselves. Daily doubles can either be assigned to specific questions, or a specific number can be designated per round which will be randomly assigned to (non-null) questions on the board.
14 |
15 | Questions can also be designated as "final" to have question screen display Final Jeopardy rather than Daily Double, and rounds themselves can have _mode_ "final" in order to stop progression after that round (future rounds can still be selected via dropdown, in the event of tiebreakers or additional rounds). Note that designating a round with mode final does not enforce wager logic (i.e. a normal round could be designated the final round).
16 |
17 | Media support is currently being implemented, by way of the `media` property on a question. Question media has a type (audio, image, video), a path (URL), and various metadata properties (primarily: `start`, `stop` for audio/video, `width`, `height` for image/video). If a question has available media, the media can be (dis)played via console. For browser security reasons, media unfortunately **cannot be specified via a local path**. However, local files can be served on a URL by spinning up a local server in the desired directory; I recommend [http-server](https://www.npmjs.com/package/http-server), which supports range requests (unfortunately, Python `http.server` and `SimpleHTTPServer` do not, which prevents custom start points, at least in Google Chrome).
18 |
19 | ## Known Limitations
20 |
21 | J-Archive is wonderful, but their volunteers only archive the _text_ content of questions seen in episodes. Jeoparizer acts as a front-end to make J-Archive records playable, so questions which may have originally had associated media (e.g. "The building seen here...") or questions never chosen by players cannot be recreated.
22 |
23 | ## Credits
24 |
25 | - All credit to the dedicated archivists of J-Archive for the ability to play past games; non-custom games would not be possible otherwise!
26 | - BroadcastChannel polyfill courtesy of [pubkey/broadcast-channel](https://github.com/pubkey/broadcast-channel)
27 |
28 |
29 | ## What's the deal with the /app/ folder?
30 |
31 | Jeopardizer was originally a Java applet, revamped as a webapp for increased usability. Though hopefully functional, it hasn't been tested in several years, and is not actively maintained. **As of 02/12/24, all scraped data from J-Archive has been removed from the repo.**
32 |
33 | Installation and controls are provided if desired:
34 |
35 |
36 | Jeopardizer App Setup
37 |
38 | ### Controls
39 |
40 | - Questions can be accessed by clicking their respective squares
41 | - Backspace will dock a player for a wrong answer, Enter will award points and escape the question.
42 | - A 5s timer can be intiated by pressing Control. If the players time out, or otherwise, tab can be used to exit the question without awarding/substracting points
43 | - The host can hit the left and right arrow keys to switch between players for score awarding
44 | - The tilde key will switch between rounds, or play sound effects if a question is active (incorrect/buzzer timeout noise, Final Jeopardy music)
45 | - Shift will pull up the scores for players, but this can only be accessed from the question menu
46 | - If a Daily Double question is opened, a wager must be input using non-numpad number keys (haven't actually tested with numpad though), hyphen/subtract key will remove the last number (-), and equals key (=) will input that wager, which can then be awarded/subtracted normally. Slash (/) will show the question if not visible after wager is input, or during Final Jeopardy.
47 | - As a failsafe for an accidental misclick, a "wager" value can also be inputted outside of a question, and awarded to a player.
48 |
49 | ### Categories
50 |
51 | By default, rounds in Jeopardizer are loaded from the single, double, and final jeopardy files located in data/questions/all, which are created by scraping J-Archive. Categories are randomly selected from these past files to create a standard Jeopardy game (6 categories for Single Jeopardy, 6 categories for Double Jeopardy, 1 category for Final Jeopardy), with the option for user-specified category count and date range are provided. Content-based filters are unlikely to be implemented as J-Archive is untagged.
52 |
53 | Specific games are require a J-Archive URL, which is then scraped to create files in data/questions/custom, rather than the random category selection. This requires an internet connection.
54 |
55 | ### Custom Games
56 |
57 | Custom games can be created by adding custom JSON files to data/questions/custom, based on the following structure:
58 |
59 | ```javascript
60 | [
61 | {"Category":"CategoryName",
62 | "Clues":[
63 | {
64 | "Question":"QuestionText", //Required
65 | "Answer":"AnswerText", //Required
66 | "Media":{ //Optional; parameters required
67 | "Name":"MediaName",
68 | "Type":"Audio | Video | Image", //Must be one of these
69 | "Path":"Filepath",
70 | }
71 | },
72 | {
73 | "Question":"...",
74 | "Answer":"...",
75 | "Media":{
76 | ...
77 | }
78 | }
79 | ...
80 | ],
81 | "Date":"MM/DD/YYYY" //Optional, used for date filter
82 | },
83 | ...
84 | ]
85 | ```
86 |
87 | Alternatively, simple categories can be created by adding plain text files to the process folder, with the following format:
88 |
89 | ```css
90 | Category1|Q1|A1|Q2|A2|Q3|A3|Q4|A4|Q5|A5
91 | Category2|Q1|A1|Q2|A2|Q3|A3|Q4|A4|Q5|A5
92 | ...
93 | CategoryN|Q1|A1|Q2|A2|Q3|A3|Q4|A4|Q5|A5
94 | ```
95 | Running either customs.py or Jeopardizer will move these files to data/questions/custom, where they can be loaded in as custom categories; this does not yet support media specifications, though those can be manually added to the generated JSON files.
96 |
97 | ### Dependencies
98 |
99 | Game (Java)
100 |
101 |
102 | -
103 | Processing (Maven: org.processing:pdf:3.3.7)
104 |
105 | - All graphics-related code in this program is handled by Processing as the main graphics library.
106 | - Java 1.7 or 1.8 must be used, as 1.9 is non-functional with Processing, and ThreadLocalRandom was only implemented in 1.8.
107 |
108 |
109 | -
110 | json-simple (Maven: com.googlecode.json-simple:json-simple:1.1.1)
111 |
112 | - json-simple is required used in order to parse the question json files and read them in as categories.
113 |
114 |
115 | -
116 | Minim (Maven: net.compartmental.code:minim:2.2.2)
117 |
118 | - Minim is used to play Jeopardy SFX—possibly overkill, but JavaSound felt like an ordeal
119 |
120 |
121 |
122 |
123 | Scraper (Python)
124 |
125 |
126 | -
127 | BeautifulSoup
128 |
- BeautifulSoup is used for to scrape J-Archive's game files, so install this in order to run the scraper
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/app/src/Question.java:
--------------------------------------------------------------------------------
1 | import processing.core.PApplet;
2 | import processing.core.PImage;
3 | import ddf.minim.AudioPlayer;
4 |
5 | public class Question {
6 | private static Question selected = null;
7 | private static PApplet gui;
8 |
9 | private int value;
10 | private String valueText = "";
11 |
12 | private Media media = null;
13 | private boolean showMedia = false;
14 |
15 | private String question;
16 | private String answer;
17 | private String dialogue;
18 |
19 | private String category;
20 | private String date;
21 |
22 | private boolean answered = false;
23 | private boolean dailyDouble = false;
24 | private boolean wagerable = false;
25 | private boolean showQuestion = true;
26 |
27 | private static float width, height;
28 | private static float widthBuffer, heightBuffer;
29 |
30 | private float x, y;
31 |
32 | public Question(String _question, String _answer) {
33 | question = _question;
34 | answer = _answer;
35 | }
36 |
37 | public Question(String _question, String _answer, int _value, Media _media) {
38 | question = _question;
39 | answer = _answer;
40 | value = _value;
41 | media = _media;
42 | }
43 |
44 | public Question() {
45 |
46 | }
47 |
48 | public static void setConstants(PApplet _gui) {
49 | Question.setGui(_gui);
50 | Question.setWidth();
51 | Question.setHeight();
52 | Question.setWidthBuffer();
53 | Question.setHeightBuffer();
54 | }
55 |
56 | public void draw() {
57 | if(isSelected(this)) {
58 | if(!showMedia) {
59 | gui.fill(PApplet.unhex(JConstants.JEOPARDY_BLUE));
60 | gui.rect(0, 0, gui.width, gui.height);
61 |
62 | if (Game.getCategoryFont() != null && Game.isUseCustomFonts()) {
63 | gui.textFont(Game.getCategoryFont());
64 | }
65 | gui.textSize(35);
66 |
67 | if (wagerable) {
68 | gui.fill(PApplet.unhex(JConstants.JEOPARDY_WAGERABLE));
69 | if (!dailyDouble && wagerable) {
70 | gui.text("FINAL JEOPARDY", gui.width / 2.0f - 0.5f * gui.textWidth("FINAL JEOPARDY"), 0 + gui.height / 10.0f); //Need to handle final jeopardy here
71 | } else {
72 | gui.text("DAILY DOUBLE", gui.width / 2.0f - 0.5f * gui.textWidth("DAILY DOUBLE"), 0 + gui.height / 10.0f); //Need to handle final jeopardy here
73 | }
74 | }
75 | if (showQuestion) {
76 | gui.fill(PApplet.unhex(JConstants.JEOPARDY_YELLOW));
77 | gui.text(valueText, gui.width / 2.0f - 0.5f * gui.textWidth(valueText), 0 + gui.height / 6.92f); //Need to handle final jeopardy here
78 | }
79 | gui.fill(255);
80 | if(category != null) {
81 | gui.text(category, gui.width / 2.0f - 0.5f * gui.textWidth(category), 0 + gui.height / 5.0f);
82 | }
83 | if (Game.getQuestionFont() != null && Game.isUseCustomFonts()) {
84 | gui.textFont(Game.getQuestionFont());
85 | gui.textSize(60);
86 | } else {
87 | gui.textSize(40);
88 | }
89 | if(showQuestion) {
90 | gui.text(question.toUpperCase(), gui.width / 8.0f, gui.height / 3.0f, gui.width - gui.width / 3.0f, gui.height);
91 | }
92 | } else {
93 | switch(media.getType()) {
94 | case IMAGE:
95 | gui.image((PImage)media.getMedia(), 0, 0, gui.width, gui.height);
96 | break;
97 | case VIDEO:
98 | break;
99 | case AUDIO:
100 | AudioPlayer m = (AudioPlayer)media.getMedia();
101 | if(m.isPlaying()) {
102 | m.pause();
103 | m.rewind();
104 | } else {
105 | m.play();
106 | }
107 | showMedia = false;
108 | break;
109 | }
110 | }
111 | } else {
112 | if (!answered) {
113 | if(Game.getMoneyFont() != null && Game.isUseCustomFonts()) {
114 | gui.textFont(Game.getMoneyFont());
115 | gui.textSize(60);
116 | } else {
117 | gui.textSize(45);
118 | }
119 |
120 | gui.fill(PApplet.unhex(JConstants.JEOPARDY_BLUE));
121 | gui.rect(x, y, width, height);
122 | gui.fill(255);
123 | gui.fill(PApplet.unhex(JConstants.JEOPARDY_YELLOW));
124 | gui.text(valueText, x + width / 2.0f - 0.5f * gui.textWidth(valueText), y + height / 2.0f + 0.5f * gui.textAscent());
125 | } else {
126 | gui.fill(PApplet.unhex(JConstants.JEOPARDY_BLUE));
127 | gui.rect(x, y, width, height);
128 | }
129 | }
130 | }
131 |
132 | public Media getMedia() {
133 | return media;
134 | }
135 | public boolean hasMedia() {
136 | return media != null;
137 | }
138 | public void setMedia(Media _media) {
139 | media = _media;
140 | }
141 |
142 | public boolean isShowMedia() {
143 | return showMedia;
144 | }
145 | public void setShowMedia(boolean _showMedia) {
146 | showMedia = _showMedia;
147 | }
148 |
149 | public boolean isAnswered() {
150 | return answered;
151 | }
152 | public void setAnswered(boolean _answered) {
153 | answered = _answered;
154 | }
155 |
156 | public String getAnswer() {
157 | return answer;
158 | }
159 | public void setAnswer(String _answer) {
160 | answer = _answer;
161 | }
162 |
163 | public String getQuestion() {
164 | return question;
165 | }
166 | public void setQuestion(String _question) {
167 | question = _question;
168 | }
169 |
170 | public String getCategory() {
171 | return category;
172 | }
173 |
174 | public void setCategory(String _category) {
175 | category = _category;
176 | }
177 |
178 | public boolean isWagerable() {
179 | return wagerable || dailyDouble;
180 | }
181 | public void setWagerable(boolean _wagerable) {
182 | wagerable = _wagerable;
183 | showQuestion = false;
184 | value = 0;
185 | }
186 |
187 | public boolean isDailyDouble() {
188 | return dailyDouble;
189 | }
190 | public void setDailyDouble(boolean _dailyDouble) {
191 | dailyDouble = _dailyDouble;
192 | wagerable = _dailyDouble;
193 | showQuestion = !_dailyDouble;
194 | value = 0;
195 | }
196 |
197 | public boolean isShowQuestion() {
198 | return showQuestion;
199 | }
200 | public void setShowQuestion(boolean _showQuestion) {
201 | showQuestion = _showQuestion;
202 | }
203 |
204 |
205 | public String getDialogue() {
206 | return dialogue;
207 | }
208 | public void setDialogue(String _dialogue) {
209 | dialogue = _dialogue;
210 | }
211 |
212 | public int getValue() {
213 | return value;
214 | }
215 | public void setValue(int _value) {
216 | value = _value;
217 | valueText = "$" + String.valueOf(value);
218 | }
219 |
220 | public String getDate() {
221 | return date;
222 | }
223 | public int getDay() {
224 | return date.length() > 0 ? Integer.parseInt(date.substring(date.indexOf("/")+1, date.lastIndexOf("/"))) : -1;
225 | }
226 | public int getMonth() {
227 | return date.length() > 0 ? Integer.parseInt(date.substring(0, date.indexOf("/"))) : -1;
228 | }
229 | public int getYear() {
230 | return date.length() > 0 ? Integer.parseInt(date.substring(date.lastIndexOf("/")+1)) : -1;
231 | }
232 | public String getMonthName() {
233 | return date.length() > 0 ? JConstants.MONTHS[getMonth()-1] : "";
234 | }
235 | public void setDate(String _date) {
236 | date = _date;
237 | }
238 |
239 | public static PApplet getGui() {
240 | return gui;
241 | }
242 | public static void setGui(PApplet _gui) {
243 | gui = _gui;
244 | }
245 |
246 | public float getX() {
247 | return x;
248 | }
249 | public void setX(float _x) {
250 | x = _x;
251 | }
252 |
253 | public float getY() {
254 | return y;
255 | }
256 | public void setY(float _y) {
257 | y = _y;
258 | }
259 |
260 | public static float getWidth() {
261 | return width;
262 | }
263 | public static void setWidth() {
264 | width = gui.width/6.125f;
265 | }
266 |
267 | public static float getHeight() {
268 | return height;
269 | }
270 | public static void setHeight() {
271 | height = gui.height/6.25f;
272 | }
273 |
274 | public static float getWidthBuffer() {
275 | return widthBuffer;
276 | }
277 | public static void setWidthBuffer() {
278 | widthBuffer = gui.width/288.0f;
279 | }
280 |
281 | public static float getHeightBuffer() {
282 | return heightBuffer;
283 | }
284 | public static void setHeightBuffer() {
285 | heightBuffer = gui.height/150.0f;
286 | }
287 |
288 | public static boolean isSelected(Question q) {
289 | return q.equals(selected);
290 | }
291 | public static Question getSelected() {
292 | return selected;
293 | }
294 | public static void setSelected(Question _selected) {
295 | selected = _selected;
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/app/scripts/scraper.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3.7
2 |
3 | from requests import get
4 | from requests.exceptions import RequestException
5 | from contextlib import closing
6 | from bs4 import BeautifulSoup, SoupStrainer
7 |
8 | import os
9 | import sys
10 | import json
11 |
12 | def simple_get(url):
13 | try:
14 | with closing(get(url, stream=True)) as resp:
15 | if is_good_response(resp):
16 | return resp.content
17 | else:
18 | print(resp.status_code)
19 | return None
20 | except RequestException as e:
21 | return None
22 |
23 | def is_good_response(resp):
24 | """
25 | Returns True if the response seems to be HTML, False otherwise.
26 | """
27 | content_type = resp.headers['Content-Type'].lower()
28 | return resp.status_code == 200 and content_type is not None and content_type.find('html') > -1
29 |
30 | site = "http://www.j-archive.com/"
31 |
32 | months = {
33 | "January":1,
34 | "February":2,
35 | "March":3,
36 | "April":4,
37 | "May":5,
38 | "June":6,
39 | "July":7,
40 | "August":8,
41 | "September":9,
42 | "October":10,
43 | "November":11,
44 | "December":12
45 | }
46 |
47 | #This function should scrape every Jeopardy game off the site, and add them to an array
48 | def scrape_games(start_point=7):
49 | archive = BeautifulSoup(simple_get(site + "listseasons.php"), parse_only=SoupStrainer('a'), features="html.parser")
50 |
51 | # Account for links in header
52 | x = iter(archive)
53 | for i in range(start_point): # 7 for S35, 10 for S32, 29 for S13
54 | x.__next__()
55 |
56 | for season in x:
57 | if season.has_attr('href'):
58 | lt = season.get("href")
59 | if lt.__contains__("showseason.php"):
60 | episode_list = BeautifulSoup(simple_get(site + lt[lt.find("showseason.php"):]), parse_only=SoupStrainer('a'), features="html.parser")
61 | season_name = str(lt[lt.find("showseason.php?")+15:]).replace("=", "_")
62 | read_season(episode_list, season_name)
63 |
64 | def scrape_game(link, path, name, individual=False):
65 | questions = BeautifulSoup(simple_get(link), features="html.parser")
66 | questions.prettify()
67 | if len(questions.find_all('table', {'class': 'round'})) + len(
68 | questions.find_all('table', {'class': 'final_round'})) != 3:
69 | print("Questions from " + link + " cannot be read due to the game type! Skipping...")
70 | else:
71 | print("Grabbing questions from " + link + "!")
72 | categories = [q.text.strip() for q in questions.find_all('td', {'class': 'category'})]
73 |
74 | game_header = questions.find(attrs={'id': "game_title"}).text ##Get title
75 | date = (game_header[game_header.find(",") + 2:]).split() ##Get date of game
76 | timestamp = str(months[date[0]]) + "/" + str(date[1][:-1]) + "/" + str(date[2]) ##Format date as M/D/Y
77 |
78 | choices = questions.find_all('td', {'class': 'clue'})
79 | choices.append(questions.find_all('td', {'class': 'category'})[-1])
80 |
81 | clues = [c.text[2:-2].strip() for c in choices]
82 | answers = []
83 |
84 | for elem in choices:
85 | r = elem.find('div')
86 | if r is not None:
87 | ans = r["onmouseover"].replace(">", ">").replace("<", "<").replace("\"",
88 | '"').replace(
89 | '\\"correct_response\\"', '"correct_response"')
90 | answers.append(
91 | ans[ans.find('correct_response">') + 18:ans.find("", "").replace(
92 | "", "").strip()) #18 is the num of chars of the text
93 | else:
94 | if answers.__len__() == 60:
95 | pass
96 | else:
97 | answers.append("")
98 |
99 | single_jeopardy_categories = categories[:6]
100 | double_jeopardy_categories = categories[6:12]
101 | final_jeopardy_categories = categories[-1]
102 |
103 | single_jeopardy = []
104 | double_jeopardy = []
105 | final_jeopardy = []
106 |
107 | qa_set = []
108 |
109 | single_blank_index = []
110 | double_blank_index = []
111 |
112 | for a in range(0, 61):
113 | if clues[a].find("\n\n\n\n\n\n\n") != -1 and len(
114 | clues[a].strip()) > 0: # Handles space between dollar amount/pick order and question
115 | qa_set.append( # Single Jeopardy
116 | {
117 | "Question": clues[a][clues[a].find("\n\n\n\n\n\n\n") + 7:].strip(),
118 | "Answer": answers[a].strip()
119 | }
120 | )
121 | elif len(clues[a].strip()) > 0:
122 | qa_set.append( # Final Jeopardy
123 | {
124 | "Question": clues[a],
125 | "Answer": answers[a]
126 | }
127 | ) # Final Jeopardy
128 | else:
129 | qa_set.append( # Append blank for mod math but log index in order to replace it
130 | {
131 | "Question": "",
132 | "Answer": ""
133 | }
134 | )
135 | if a < 30:
136 | single_blank_index.append(a)
137 | else:
138 | double_blank_index.append(a)
139 |
140 | i = 0
141 | for c in single_jeopardy_categories:
142 | single_jeopardy.append(
143 | {
144 | "Category": c,
145 | "Clues": qa_set[i:30:6],
146 | "Date": timestamp
147 | }
148 | )
149 | i += 1
150 |
151 | i = 0
152 | for c in double_jeopardy_categories:
153 | double_jeopardy.append(
154 | {
155 | "Category": c,
156 | "Clues": qa_set[30 + i:60:6],
157 | "Date": timestamp
158 | }
159 | )
160 | i += 1
161 |
162 | final_jeopardy.append(
163 | {
164 | "Category": final_jeopardy_categories,
165 | "Clues": [qa_set[-1]],
166 | "Date": timestamp
167 | }
168 | )
169 |
170 | single_blank_unique = []
171 | double_blank_unique = []
172 |
173 | for removal in single_blank_index:
174 | if (removal % 6) not in single_blank_unique:
175 | single_blank_unique.append(removal % 6)
176 |
177 | for removal in double_blank_index:
178 | if (removal % 6) not in double_blank_unique:
179 | double_blank_unique.append(removal % 6)
180 |
181 | if single_blank_unique.__len__() > 0:
182 | single_blank_unique.sort()
183 | single_blank_unique.reverse()
184 |
185 | if double_blank_unique.__len__() > 0:
186 | double_blank_unique.sort()
187 | double_blank_unique.reverse()
188 |
189 | print(single_blank_unique)
190 | print(double_blank_unique)
191 | print(timestamp)
192 |
193 | for index in single_blank_unique:
194 | single_jeopardy.pop(index)
195 |
196 | for index in double_blank_unique:
197 | double_jeopardy.pop(index)
198 |
199 | print(single_jeopardy)
200 | print(double_jeopardy)
201 | print(final_jeopardy)
202 |
203 | with open(os.path.join(path, "single_jeopardy_" + name + ".json"), 'a+' if not individual else 'w+') as sj:
204 | if individual:
205 | sj.write("[")
206 | for c in single_jeopardy:
207 | json.dump(c, sj)
208 | sj.write(",")
209 |
210 | with open(os.path.join(path, "double_jeopardy_" + name + ".json"), 'a+' if not individual else 'w+') as dj:
211 | if individual:
212 | dj.write("[")
213 | for c in double_jeopardy:
214 | json.dump(c, dj)
215 | dj.write(",")
216 |
217 | with open(os.path.join(path, "final_jeopardy_" + name + ".json"), 'a+' if not individual else 'w+') as fj:
218 | if individual:
219 | fj.write("[")
220 | for c in final_jeopardy:
221 | json.dump(c, fj)
222 | fj.write(",")
223 | if individual:
224 | add_end_bracket(path)
225 |
226 | def read_season(episode_list, season_name): #Episode list should be beautifulsoup, season_name is the name of the outputted file
227 | with open(os.path.join(os.path.dirname(__file__), "..", "process", "by_season", "single_jeopardy_season" + season_name + ".json"), "w+") as sj: sj.write("[")
228 | with open(os.path.join(os.path.dirname(__file__), "..", "process", "by_season", "double_jeopardy_season" + season_name + ".json"), "w+") as dj: dj.write("[")
229 | with open(os.path.join(os.path.dirname(__file__), "..", "process", "by_season", "final_jeopardy_season" + season_name + ".json"), "w+") as fj: fj.write("[")
230 |
231 | for episode in episode_list:
232 | li = episode.get("href")
233 | if li.__contains__("showgame.php"):
234 | try:
235 | scrape_game(site + li[li.find("showgame.php"):], os.path.join(os.path.dirname(__file__), "..", "process", "by_season"), season_name)
236 | except:
237 | print("Something went wrong and an exception was thrown! Link was " + li[li.find("showgame.php"):])
238 |
239 | add_end_bracket(os.path.join(os.path.dirname(__file__), "..", "process", "by_season"))
240 |
241 | def add_end_bracket(path):
242 | files = os.listdir(path)
243 |
244 | for f in files:
245 | with open(os.path.join(path, f), 'rb+') as bfile:
246 | bfile.seek(-1, os.SEEK_END)
247 | bfile.truncate()
248 | with open(os.path.join(path, f), 'a') as bfile:
249 | bfile.write("]")
250 |
251 | def combine_files(path=os.path.join(os.path.dirname(__file__), "..", "data", "questions")):
252 | names = ["single_jeopardy", "double_jeopardy", "final_jeopardy"]
253 |
254 | for name in names:
255 | with open(os.path.join(path, "all", name + ".json"), "w+") as cat:
256 | cat.write("[")
257 | for file in os.listdir(os.path.join(path, "by_season")):
258 | if file.startswith(name):
259 | with(open(os.path.join(path, "by_season", file), 'r')) as add:
260 | questions = json.load(add)
261 | for q in questions:
262 | json.dump(q, cat)
263 | cat.write(",")
264 |
265 | add_end_bracket(os.path.join(path, "all"))
266 |
267 | if __name__ == "__main__":
268 | # read_season(BeautifulSoup(simple_get("http://www.j-archive.com/showseason.php?season=superjeopardy"), parse_only=SoupStrainer('a'),features="html.parser"), "superjeopardy")
269 | # scrape_game("http://wwreaw.j-archive.com/showgame.php?game_id=6302", "/Users/zackamiton/Code/Jeopardizer/data/questions/scrape", "scraped", True)
270 | if len(sys.argv) == 3: #Mode called from the Game script
271 | if str(sys.argv[1]).lower() == "-s":
272 | scrape_game(sys.argv[2], os.path.join(os.path.dirname(__file__), "..", "data", "questions", "scrape"), "scraped", True)
273 | else:
274 | read_season(BeautifulSoup(simple_get("http://www.j-archive.com/showseason.php?season=35"), parse_only=SoupStrainer('a'), features="html.parser"), "35")
275 | pass
276 |
277 | pass
278 |
279 |
280 |
281 |
--------------------------------------------------------------------------------
/media/custom.json:
--------------------------------------------------------------------------------
1 | {
2 | "rounds": [
3 | {
4 | "name": "start-here",
5 | "dds": 1,
6 | "categories":[
7 | {
8 | "category":"Sighs & Blunders",
9 | "comments":"This is a test comment!",
10 | "clues":[
11 | {
12 | "question":"TestQ",
13 | "answer":"TestA"
14 | },
15 | {
16 | "question":"TestQ",
17 | "answer":"TestA"
18 | },
19 | {
20 | "question":"TestQ",
21 | "answer":"TestA"
22 | },
23 | {
24 | "question":"",
25 | "answer":""
26 | },
27 | {
28 | "question":"TestQ",
29 | "answer":"TestA"
30 | }
31 | ]
32 | },
33 | {
34 | "category":"Sighs and Blunders III: Revenge of the Category",
35 | "comments":"Hey, where'd the 2nd category go? Oh, I guess this round only has 5!",
36 | "clues":[
37 | {
38 | "question":"TestQ",
39 | "answer":"TestA"
40 | },
41 | {
42 | "question":"TestQ",
43 | "answer":"TestA"
44 | },
45 | {
46 | "question":"TestQ",
47 | "answer":"TestA"
48 | },
49 | {
50 | "question":"TestQ",
51 | "answer":"TestA"
52 | },
53 | {
54 | "question":"TestQ",
55 | "answer":"TestA"
56 | }
57 | ]
58 | },
59 | {
60 | "category":"Sighs & Blunders 4: The Goblet of Fire Questions",
61 | "clues":[
62 | {
63 | "question":"TestQ",
64 | "answer":"TestA"
65 | },
66 | {
67 | "question":"TestQ",
68 | "answer":"TestA"
69 | },
70 | {
71 | "question":"TestQ",
72 | "answer":"TestA"
73 | },
74 | {
75 | "question":"TestQ",
76 | "answer":"TestA"
77 | },
78 | {
79 | "question":"TestQ",
80 | "answer":"TestA"
81 | }
82 | ]
83 | },
84 | {
85 | "category":"Sighs & Blunders 5: Vice",
86 | "comments":"This is a test comment!",
87 | "clues":[
88 | {
89 | "question":"TestQ",
90 | "answer":"TestA"
91 | },
92 | {
93 | "question":"TestQ",
94 | "answer":"TestA"
95 | },
96 | {
97 | "question":"TestQ",
98 | "answer":"TestA"
99 | },
100 | {
101 | "question":"TestQ",
102 | "answer":"TestA"
103 | },
104 | {
105 | "question":"TestQ",
106 | "answer":"TestA"
107 | }
108 | ]
109 | },
110 | {
111 | "category":"Sighs & Blunders 6: The Last One",
112 | "comments":"This is a test comment!",
113 | "clues":[
114 | {
115 | "question":"TestQ",
116 | "answer":"TestA"
117 | },
118 | {
119 | "question":"TestQ",
120 | "answer":"TestA"
121 | },
122 | {
123 | "question":"TestQ",
124 | "answer":"TestA"
125 | },
126 | {
127 | "question":"TestQ",
128 | "answer":"TestA"
129 | },
130 | {
131 | "question":"TestQ",
132 | "answer":"TestA"
133 | }
134 | ]
135 | }
136 | ]
137 | },
138 | {
139 | "name": "non-standard-dimensions",
140 | "dds": 2,
141 | "multiplier": 2,
142 | "categories": [
143 | {
144 | "category":"Sighs & Blunders",
145 | "clues":[{"question":"TestQ", "answer":"TestA"}]
146 | },
147 | {
148 | "category":"Sighs & Blunders",
149 | "clues":[{"question":"TestQ", "answer":"TestA"}]
150 | },
151 | {
152 | "category":"Sighs & Blunders",
153 | "clues":[{"question":"TestQ", "answer":"TestA"}]
154 | },
155 | {
156 | "category":"Sighs & Blunders",
157 | "clues":[{"question":"TestQ", "answer":"TestA"}]
158 | },
159 | {
160 | "category":"Sighs & Blunders",
161 | "clues":[{"question":"TestQ", "answer":"TestA"}]
162 | },
163 | {
164 | "category":"Sighs & Blunders",
165 | "clues":[{"question":"TestQ", "answer":"TestA"}]
166 | },
167 | {
168 | "category":"Sighs & Blunders",
169 | "clues":[{"question":"TestQ", "answer":"TestA"}]
170 | },
171 | {
172 | "category":"Sighs & Blunders",
173 | "clues":[{"question":"TestQ", "answer":"TestA"}]
174 | },
175 | {
176 | "category":"Sighs & Blunders",
177 | "comment":"The game board scales to have as many rows as the most category",
178 | "clues":[
179 | {
180 | "question":"TestQ",
181 | "answer":"TestA"
182 | },
183 | {
184 | "question":"Sup?",
185 | "answer": "nm"
186 | },
187 | {
188 | "question":"TestQ",
189 | "answer":"TestA"
190 | }
191 | ]
192 | }
193 | ]
194 | },
195 | {
196 | "name": "multiply-it-up",
197 | "multiplier": 6.5,
198 | "categories": [
199 | {
200 | "category":"DJ1",
201 | "comments":"yawn",
202 | "clues":[
203 | {
204 | "question":"TestQ",
205 | "answer":"TestA"
206 | },
207 | {
208 | "question":"TestQ",
209 | "answer":"TestA"
210 | },
211 | {
212 | "question":"TestQ",
213 | "answer":"TestA"
214 | },
215 | {
216 | "question":"TestQ",
217 | "answer":"TestA"
218 | },
219 | {
220 | "question":"TestQ",
221 | "answer":"TestA"
222 | }
223 | ]
224 | },
225 | {
226 | "category":"DJ2",
227 | "clues":[
228 | {
229 | "question":"TestQ",
230 | "answer":"TestA"
231 | },
232 | {
233 | "question":"TestQ",
234 | "answer":"TestA"
235 | },
236 | {
237 | "question":"TestQ",
238 | "answer":"TestA"
239 | },
240 | {
241 | "question":"TestQ",
242 | "answer":"TestA"
243 | },
244 | {
245 | "question":"TestQ",
246 | "answer":"TestA"
247 | }
248 | ]
249 | },
250 | {
251 | "category":"DJ3",
252 | "clues":[
253 | {
254 | "question":"TestQ",
255 | "answer":"TestA"
256 | },
257 | {
258 | "question":"TestQ",
259 | "answer":"TestA"
260 | },
261 | {
262 | "question":"TestQ",
263 | "answer":"TestA"
264 | },
265 | {
266 | "question":"TestQ",
267 | "answer":"TestA"
268 | },
269 | {
270 | "question":"TestQ",
271 | "answer":"TestA"
272 | }
273 | ]
274 | }
275 | ]
276 | },
277 | {
278 | "name": "oops-all-doubles",
279 | "dds": 10,
280 | "categories": [
281 | {
282 | "category":"DD1",
283 | "clues":[
284 | {
285 | "question":"TestQ",
286 | "answer":"TestA"
287 | },
288 | {
289 | "question":"TestQ",
290 | "answer":"TestA"
291 | },
292 | {
293 | "question":"TestQ",
294 | "answer":"TestA"
295 | },
296 | {
297 | "question":"TestQ",
298 | "answer":"TestA"
299 | },
300 | {
301 | "question":"TestQ",
302 | "answer":"TestA"
303 | }
304 | ]
305 | },
306 | {
307 | "category":"DD2",
308 | "clues":[
309 | {
310 | "question":"TestQ",
311 | "answer":"TestA"
312 | },
313 | {
314 | "question":"TestQ",
315 | "answer":"TestA"
316 | },
317 | {
318 | "question":"TestQ",
319 | "answer":"TestA"
320 | },
321 | {
322 | "question":"TestQ",
323 | "answer":"TestA"
324 | },
325 | {
326 | "question":"TestQ",
327 | "answer":"TestA"
328 | }
329 | ]
330 | }
331 | ]
332 | },
333 | {
334 | "name": "custom-media",
335 | "categories": [
336 | {
337 | "category": "Name That Tune!",
338 | "comment": "Audio test",
339 | "clues": [
340 | {
341 | "question": "This song has a lot of bleeps and bloops",
342 | "answer": "SoundHelix Song 1 (idk it was stock audio I found online)",
343 | "media": {
344 | "type": "audio",
345 | "path": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
346 | "metadata": {
347 | "start": 50,
348 | "end": 60
349 | }
350 | }
351 | }
352 | ]
353 | },
354 | {
355 | "category": "Picture This!",
356 | "comment": "Image test",
357 | "clues": [
358 | {
359 | "question": "The image seen here is not a natural number",
360 | "answer": "Zero",
361 | "media": {
362 | "type": "image",
363 | "path": "https://d138zd1ktt9iqe.cloudfront.net/media/seo_landing_files/zero-is-not-a-natural-number-1612769491.png",
364 | "metadata": {
365 | "width": 781.5,
366 | "height": 560.25
367 | }
368 | }
369 | }
370 | ]
371 | },
372 | {
373 | "category": "Heartbreak Feels Good!",
374 | "comment": "Video test",
375 | "clues": [
376 | {
377 | "question": "The planet seen here goes round and round...",
378 | "answer": "Earth",
379 | "media": {
380 | "type": "video",
381 | "path": "https://file-examples.com/storage/fe332cf53a63a4bd5991eb4/2017/04/file_example_MP4_480_1_5MG.mp4",
382 | "metadata": {
383 | "width": 480,
384 | "height": 270,
385 | "start": 20,
386 | "end": 30
387 | }
388 | }
389 | }
390 | ]
391 | }
392 | ]
393 | },
394 | {
395 | "name": "nonstandard-final",
396 | "mode": "final",
397 | "categories": [
398 | {
399 | "category": "Sample Final",
400 | "comment": "Marking as final makes progress round throw up the score screen, but DOES NOT display final jeopardy/wager logic on questions (this is by question). Extra rounds can also still be added/accessed!",
401 | "clues":[
402 | {
403 | "question":"TestQ",
404 | "answer":"TestA"
405 | },
406 | {
407 | "question":"TestQ",
408 | "answer":"TestA"
409 | },
410 | {
411 | "question":"TestQ",
412 | "answer":"TestA"
413 | },
414 | {
415 | "question":"TestQ",
416 | "answer":"TestA"
417 | },
418 | {
419 | "question":"TestQ",
420 | "answer":"TestA",
421 | "final": true
422 | }
423 | ]
424 | }
425 | ]
426 | },
427 | {
428 | "name": "post-final-content",
429 | "categories": [
430 | {
431 | "category": "Custom Values",
432 | "comment": "This could be used as a tiebreaker, or just optional rounds. Using it to showcase misc other properties in this sample file!",
433 | "clues": [
434 | {"question": "ABC", "answer": "DEF", "value": 100000},
435 | {"question": "ABC", "answer": "DEF", "value": -25},
436 | {"question": "ABC", "answer": "DEF", "dd": true}
437 | ]
438 | }
439 | ]
440 | }
441 | ]
442 | }
443 |
--------------------------------------------------------------------------------
/docs/console.js:
--------------------------------------------------------------------------------
1 | const bc = new BroadcastChannel2('Jeopardizer')
2 |
3 | const mainDiv = document.getElementById('main')
4 | const questionDiv = document.getElementById('question')
5 | const pauseDiv = document.getElementById('pause')
6 |
7 | const playerList = document.getElementById('player_list')
8 | const boardDisplay = document.getElementById('board_display')
9 |
10 | const currentCategory = document.getElementById("question_category")
11 | const currentQuestion = document.getElementById("question_text")
12 | const currentValue = document.getElementById("question_value")
13 | const currentAnswer = document.getElementById("question_answer")
14 | let questionValue = 0
15 | const scoreInput = document.getElementById("score_input")
16 |
17 | const addPlayer = document.getElementById("add_player")
18 | const removePlayer = document.getElementById("remove_player")
19 | const playerDropdown = document.getElementById("players")
20 |
21 | const dailyDoubleText = document.getElementById('daily_double')
22 | const wagerControls = document.getElementById('wager_controls')
23 | const wagerButton = document.getElementById('wager_button')
24 | const wagerInput = document.getElementById('wager_input')
25 | const questionButton = document.getElementById('question_button')
26 |
27 | const backButton = document.getElementById('back_button')
28 | const progressButton = document.getElementById('progress_button')
29 | const regressButton = document.getElementById('regress_button')
30 | const roundDropdown = document.getElementById('round_dropdown')
31 | const roundButton = document.getElementById('select_round')
32 |
33 | const scoresButton = document.getElementById('scores_button')
34 | const resetButton = document.getElementById('reset_button')
35 | const buzzerButton = document.getElementById('buzzer_button')
36 |
37 | const sfxDropdown = document.getElementById('sfx_dropdown')
38 |
39 | const playSFX = document.getElementById('play_sfx')
40 | const pauseSFX = document.getElementById('pause_sfx')
41 |
42 | const playMedia = document.getElementById('play_media')
43 | const pauseMedia = document.getElementById('pause_media')
44 |
45 | const timerControls = document.getElementById('timer')
46 | const timerText = document.getElementById('round_timer')
47 | const timerButton = document.getElementById('timer_button')
48 | let timerCallback;
49 |
50 | const divs = [mainDiv, questionDiv, pauseDiv]
51 | const states = ["Main", "Question"]
52 |
53 | let board = null
54 | let settings = {}
55 |
56 | let cid = null
57 | let coid = Date.now()
58 |
59 | let players = {}
60 | let timeLimit = null
61 | let currentTime = null
62 |
63 | function setState(div) {
64 | divs.forEach(d => {
65 | if(d != div) d.style.display = 'none'
66 | else d.style.display = 'block'
67 | })
68 | }
69 |
70 | function getState() {
71 | for(let i = 0; i < divs.length; i++) {
72 | if(divs[i].style.display != 'none') return states[i]
73 | }
74 | }
75 |
76 | const isQuestion = () => getState() == "Question"
77 | const isMain = () => getState() == "Main"
78 |
79 | function closeQuestion() {
80 | Array.from(document.querySelectorAll('[data-manual]')).forEach(sb => {
81 | switch(sb.dataset.manual) {
82 | case 'true':
83 | sb.style.display = 'inline';
84 | break;
85 | case 'support':
86 | // could change this, but the idea is just hidden non-inline elements for spacing
87 | sb.style.display = 'block';
88 | break;
89 | case 'false':
90 | sb.style.display = 'none';
91 | break;
92 | }
93 | })
94 | sendMessage("CLOSE_QUESTION")
95 | boardDisplay.style.display = 'block'
96 | setState(mainDiv)
97 | }
98 |
99 |
100 | window.onload = function() {
101 | backButton.addEventListener('click', closeQuestion)
102 | progressButton.addEventListener('click', () => {
103 | sendMessage("PROGRESS_ROUND")
104 | sendMessage("SHOW_BOARD")
105 | scoresButton.textContent = "Show Scores"
106 |
107 | currentTime = timeLimit
108 | timerText.textContent = getTimeText(timeLimit)
109 | clearInterval(timerCallback)
110 | timerButton.textContent = 'Start Timer'
111 | })
112 |
113 | regressButton.addEventListener('click', () => {
114 | sendMessage("REGRESS_ROUND")
115 | sendMessage("SHOW_BOARD")
116 | scoresButton.textContent = "Show Scores"
117 |
118 | currentTime = timeLimit
119 | timerText.textContent = getTimeText(timeLimit)
120 | clearInterval(timerCallback)
121 | timerButton.textContent = 'Start Timer'
122 | })
123 |
124 | roundButton.addEventListener('click', () => {
125 | sendMessage("SET_ROUND", [['roundKey', roundDropdown.options[roundDropdown.selectedIndex].value]])
126 | sendMessage("SHOW_BOARD")
127 | scoresButton.textContent = "Show Scores"
128 |
129 | currentTime = timeLimit
130 | timerText.textContent = getTimeText(timeLimit)
131 | clearInterval(timerCallback)
132 | timerButton.textContent = 'Start Timer'
133 | })
134 |
135 | questionButton.addEventListener('click', () => sendMessage('SHOW_QUESTION'))
136 | wagerButton.addEventListener('click', () => {
137 | questionValue = parseInt(wagerInput.value || 0)
138 | currentValue.textContent = `$${questionValue}`
139 | sendMessage("SET_VALUE", [['value', questionValue]])
140 | sendMessage('SHOW_QUESTION')
141 | })
142 |
143 | scoresButton.addEventListener('click', () => {
144 | if(scoresButton.textContent == "Show Scores") {
145 | if(Object.entries(players).length > 0) {
146 | sendMessage("SHOW_SCORES")
147 | scoresButton.textContent = "Show Board"
148 | }
149 | } else {
150 | sendMessage("SHOW_BOARD")
151 | scoresButton.textContent = "Show Scores"
152 | }
153 | })
154 |
155 | resetButton.addEventListener('click', restart)
156 |
157 | playSFX.addEventListener('click', () => sendMessage("PLAY_SFX", [['sfx', sfxDropdown.options[sfxDropdown.selectedIndex].value]]))
158 | pauseSFX.addEventListener('click', () => sendMessage("PAUSE_SFX"))
159 |
160 | playMedia.addEventListener('click', () => sendMessage("PLAY_MEDIA"))
161 | pauseMedia.addEventListener('click', () => sendMessage("PAUSE_MEDIA"))
162 |
163 | timerButton.addEventListener('click', () => {
164 | if(timerButton.textContent == 'Start Timer') {
165 | timerButton.textContent = 'Pause Timer'
166 | timerCallback = setInterval(countdown, 1000)
167 | } else {
168 | clearInterval(timerCallback)
169 | timerButton.textContent = 'Start Timer'
170 | }
171 | })
172 |
173 | addPlayer.addEventListener('click', () => {
174 | const name = prompt("Player Name: ");
175 | if(name && !(name in players)) {
176 | if(Object.entries(players).length === 0) {
177 | removePlayer.disabled = false;
178 | }
179 |
180 | const newOption = document.createElement('option');
181 | newOption.text = name;
182 | newOption.value = name;
183 | playerDropdown.add(newOption);
184 |
185 | players[name] = 0;
186 |
187 | sendMessage("UPDATE_PLAYERS", [['players', players]])
188 | updatePlayerList(restart=true)
189 | }
190 | })
191 |
192 | removePlayer.addEventListener('click', () => {
193 | const playerToRemove = playerDropdown.options[playerDropdown.selectedIndex].value
194 | delete players[playerToRemove]
195 | playerDropdown.remove(playerDropdown.selectedIndex);
196 |
197 | sendMessage("UPDATE_PLAYERS", [['players', players]])
198 | updatePlayerList(restart=true)
199 |
200 | if(Object.entries(players).length === 0) {
201 | removePlayer.disabled = true;
202 | }
203 | })
204 | }
205 |
206 | function countdown() {
207 | if(currentTime > 0) {
208 | if(currentTime == 1) sendMessage("PLAY_SFX", [['sfx', "Round Over"]])
209 | currentTime -= 1
210 | }
211 | timerText.textContent = getTimeText(currentTime)
212 | }
213 |
214 | function clearChildren(node) {
215 | while(node.lastChild) {
216 | node.removeChild(node.lastChild)
217 | }
218 | }
219 |
220 | function restart() {
221 | for(let node of [boardDisplay, playerList, roundDropdown, playerDropdown]) {
222 | clearChildren(node);
223 | }
224 |
225 | scoresButton.textContent = 'Show Scores'
226 | mainDiv.style.display = 'none'
227 | document.querySelector('nav').style.display = 'none'
228 | setState(pauseDiv)
229 | sendMessage("RESTART")
230 | }
231 |
232 | function getTimeText(secs) {
233 | return new Date(secs*1000).toISOString().slice((secs < 3600) ? 14 : 11, 19)
234 | }
235 |
236 | function sendMessage(action, params=[]) {
237 | let messageResponse = {
238 | src: "CONSOLE",
239 | cid: cid,
240 | coid: coid
241 | }
242 | params.forEach(p => messageResponse[p[0]] = p[1])
243 | bc.postMessage({
244 | action: action,
245 | response: messageResponse
246 | })
247 | }
248 |
249 | window.addEventListener('beforeunload', (event) => {
250 | sendMessage("CONSOLE_CLOSE")
251 | })
252 |
253 | buzzerButton.addEventListener('click', () => {
254 | const ok = confirm("Are you sure? Launching buzzers will require all players to join a new buzz.in game!")
255 | if(ok) {
256 | sendMessage("OPEN_BUZZERS")
257 | }
258 | })
259 |
260 | bc.onmessage = function(msg) {
261 | const action = msg.action
262 | const data = msg.response
263 | if(data?.src == "CLIENT") {
264 | switch(action) {
265 | case "LINK_CLIENT":
266 | console.log("Received linking message...linking console...")
267 | if(!cid) {
268 | cid = data.cid
269 | sendMessage("LINK_CONSOLE")
270 | }
271 | break
272 | case "START_GAME":
273 | if(cid == data.cid) {
274 | players = data.players
275 | board = data.board
276 | timeLimit = data.limit
277 | settings = data.settings
278 |
279 | if(timeLimit) {
280 | timerControls.style.display = 'block'
281 | currentTime = timeLimit
282 | timerText.textContent = getTimeText(currentTime)
283 | }
284 |
285 | updatePlayerList(restart=true)
286 | updateBoard()
287 | data.seen.forEach(v => {
288 | const matchedCell = document.querySelector(`[data-cell='${v}']`)
289 | if(matchedCell) matchedCell.classList.add('seen')
290 | })
291 |
292 | const playerNames = Object.keys(players)
293 |
294 | if(playerNames.length === 0) removePlayer.disabled = true;
295 | playerNames.forEach(p => {
296 | const opt = document.createElement("option")
297 | opt.text = p
298 | opt.value = p
299 | playerDropdown.add(opt)
300 | })
301 |
302 | board.forEach(b => {
303 | const key = Object.keys(b)[0]
304 | const opt = document.createElement("option")
305 | opt.text = key
306 | opt.value = key
307 | roundDropdown.add(opt)
308 | })
309 |
310 | setState(mainDiv)
311 | mainDiv.style.display = 'block'
312 |
313 | document.querySelector('nav').style.display = 'block'
314 | document.getElementById("board_display").style.display = 'block'
315 |
316 | const activeRound = document.querySelector(`#${data.roundKey}`)
317 | roundDropdown.value = data.roundKey
318 |
319 | if(activeRound) activeRound.style.display = 'block'
320 | }
321 | break
322 | case "GET_PLAYERS":
323 | if(cid == data.cid) players = data.players
324 | break
325 | case "LOAD_QUESTION":
326 | if(data.cid == cid) {
327 | currentAnswer.textContent = data.answer
328 | currentQuestion.textContent = data.question
329 | currentCategory.textContent = data.category
330 | currentValue.textContent = data.label + (data.label === `$${data.value}` ? '' : ` (${data.value})`)
331 |
332 | questionValue = parseInt(data.value)
333 |
334 | const relevantQuestion = document.querySelector(`[data-cell='${data.cell}']`)
335 | if(relevantQuestion) relevantQuestion.classList.add("seen")
336 |
337 | if(data.dd.includes('true') && settings.ddEnabled) {
338 | dailyDoubleText.textContent = "DAILY DOUBLE";
339 | dailyDoubleText.style.display = 'block'
340 | showWager(true)
341 | } else if(data.final === 'true') {
342 | dailyDoubleText.textContent = "FINAL JEOPARDY";
343 | dailyDoubleText.style.display = 'block'
344 | showWager(true)
345 | } else {
346 | dailyDoubleText.style.display = 'none'
347 | showWager(false)
348 | }
349 |
350 | Array.from(document.querySelectorAll("button[data-manual]")).forEach(sb => {
351 | sb.style.display = "inline"
352 | })
353 | setState(questionDiv)
354 | boardDisplay.style.display = 'none'
355 | const isTrue = v => ['true', true].includes(v)
356 |
357 | sendMessage("OPEN_QUESTION", params=[["dd", isTrue(data.dd) && settings.ddEnabled], ["final", data.final]])
358 | }
359 | break
360 | case "SET_ROUND":
361 | if(cid == data.cid) {
362 | Array.from(document.querySelectorAll(".round_container")).forEach(t => t.style.display = 'none')
363 |
364 | roundDropdown.value = data.roundKey
365 | const relevantRound = document.querySelector(`#${data.roundKey}`)
366 | if(relevantRound) relevantRound.style.display = 'block'
367 | }
368 | break
369 | case "NEW_GAME":
370 | break
371 | case "CLIENT_CLOSE":
372 | if(cid == data.cid) {
373 | window.close()
374 | if(buzzerWindow) buzzerWindow.close()
375 | }
376 | break
377 | case "REQUEST_SCORES":
378 | if(cid == data.cid) {
379 | if(Object.entries(players).length > 0) {
380 | sendMessage("SHOW_SCORES")
381 | scoresButton.textContent = "Show Board"
382 | }
383 | }
384 | break;
385 | default:
386 | console.log("Invalid action at console: ", msg)
387 | break
388 | }
389 | }
390 | }
391 |
392 | function updateBoard() {
393 | clearChildren(boardDisplay)
394 |
395 | board.forEach(b => {
396 | const roundKey = Object.keys(b)[0]
397 | const roundData = Object.values(b)[0]
398 |
399 | const roundDiv = document.createElement("div")
400 | roundDiv.id = roundKey
401 | roundDiv.classList.add("round_container")
402 |
403 | const roundTable = document.createElement("table")
404 |
405 | roundTable.classList.add("game_table", "console")
406 | if(settings.ddEnabled) {
407 | roundTable.classList.add("dd_enabled")
408 | }
409 |
410 | const commentList = document.createElement("ul")
411 | commentList.classList.add("comments")
412 |
413 | const headerRow = document.createElement("tr")
414 | roundData[0].forEach(cat => {
415 | const { category, comment } = cat
416 | const headerCell = document.createElement("th")
417 | headerCell.textContent = category
418 |
419 | if(comment) {
420 | const commentItem = document.createElement("li")
421 | commentItem.textContent = `${category}: ${comment.replaceAll("\n", " ")}`
422 | commentList.appendChild(commentItem)
423 | }
424 |
425 | headerRow.appendChild(headerCell)
426 | })
427 |
428 | roundTable.appendChild(headerRow)
429 | roundData.slice(1).forEach(row => {
430 | const newRow = document.createElement("tr")
431 | row.forEach(cell => {
432 | const newCell = document.createElement('td')
433 | newCell.classList.add("question_cell", "console")
434 |
435 | if(!cell.question) newCell.setAttribute("disabled", true)
436 | else {
437 | Object.entries(cell).forEach(([k, v]) => {
438 | if(!k.startsWith("client")) newCell.dataset[k] = v
439 | })
440 |
441 | newCell.textContent = cell.label
442 | if(cell.disabled) newCell.classList.add("seen")
443 | }
444 |
445 | newCell.addEventListener('click', ({target}) => {
446 | if(!target.disabled) {
447 | sendMessage("CELL_CLICKED", [['cell', target.dataset.cell]])
448 | target.classList.add("seen")
449 | }
450 | })
451 | newRow.appendChild(newCell)
452 | })
453 | roundTable.appendChild(newRow)
454 | })
455 |
456 | roundDiv.append(roundTable, commentList)
457 | document.getElementById("board_display").appendChild(roundDiv)
458 | })
459 | }
460 |
461 | function updatePlayerList(restart=false) {
462 | if(restart) {
463 | clearChildren(playerList)
464 |
465 | Object.entries(players).forEach(pe => {
466 | let pl = document.createElement('li')
467 |
468 | let playerSpan = document.createElement('span')
469 | playerSpan.textContent = `${pe[0]}: ${pe[1]}`
470 |
471 | let [subtractButton, addButton, addFromInputButton] = new Array(3).fill().map((_, idx) => {
472 | let scoreButton = document.createElement('button')
473 | scoreButton.textContent = ({
474 | 0:"- (Question)",
475 | 1:"+ (Question)",
476 | 2:"+ (Input)"
477 | })[idx] || ""
478 |
479 | scoreButton.setAttribute('data-player', pe[0])
480 | scoreButton.setAttribute('data-manual', idx == 2)
481 |
482 | let scoreCallback = ({
483 | 0: () => {
484 | if(isQuestion()) players[scoreButton.getAttribute('data-player')] -= questionValue
485 | },
486 | 1: () => {
487 | if(isQuestion()) {
488 | players[scoreButton.getAttribute('data-player')] += questionValue
489 | closeQuestion()
490 | }
491 | },
492 | 2: () => {
493 | if(scoreInput.value) players[scoreButton.getAttribute('data-player')] += parseInt(scoreInput.value)
494 | }
495 | })[idx] || ""
496 |
497 | scoreButton.addEventListener('click', function() {
498 | scoreCallback()
499 | sendMessage("UPDATE_PLAYERS", [['players', players]])
500 | updatePlayerList()
501 | })
502 | return scoreButton
503 | })
504 |
505 | pl.appendChild(addFromInputButton)
506 | pl.appendChild(addButton)
507 | pl.appendChild(subtractButton)
508 | pl.appendChild(playerSpan)
509 | playerList.appendChild(pl)
510 | })
511 | } else {
512 | const ps = Object.entries(players)
513 | Array.from(playerList.getElementsByTagName('span')).forEach((s, idx) => {
514 | s.textContent = `${ps[idx][0]}: ${ps[idx][1]}`
515 | })
516 | }
517 | }
518 |
519 | function showWager(shouldShow) {
520 | if(shouldShow) {
521 | wagerControls.style.display = 'block'
522 | } else {
523 | wagerControls.style.display = 'none'
524 | }
525 | }
526 |
--------------------------------------------------------------------------------
/docs/lib/broadcast-channel.min.js:
--------------------------------------------------------------------------------
1 | !function r(o,i,s){function a(t,e){if(!i[t]){if(!o[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(u)return u(t,!0);throw(e=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",e}n=i[t]={exports:{}},o[t][0].call(n.exports,function(e){return a(o[t][1][e]||e)},n,n.exports,r,o,i,s)}return i[t].exports}for(var u="function"==typeof require&&require,e=0;e=t&&e.fn(n.data)})},o=i.method.microSeconds(),i._prepP?i._prepP.then(function(){i._iL=!0,i.method.onMessage(i._state,r,o)}):(i._iL=!0,i.method.onMessage(i._state,r,o)))}function h(e,t,n){e._addEL[t]=e._addEL[t].filter(function(e){return e!==n});t=e;t._iL&&!d(t)&&(t._iL=!1,e=t.method.microSeconds(),t.method.onMessage(t._state,null,e))}(n.BroadcastChannel=r)._pubkey=!0,r.prototype={postMessage:function(e){if(this.closed)throw new Error("BroadcastChannel.postMessage(): Cannot post message after channel has closed "+JSON.stringify(e));return l(this,"message",e)},postInternal:function(e){return l(this,"internal",e)},set onmessage(e){var t={time:this.method.microSeconds(),fn:e};h(this,"message",this._onML),e&&"function"==typeof e?(this._onML=t,f(this,"message",t)):this._onML=null},addEventListener:function(e,t){var n=this.method.microSeconds();f(this,e,{time:n,fn:t})},removeEventListener:function(e,t){var n=this._addEL[e].find(function(e){return e.fn===t});h(this,e,n)},close:function(){var e,t=this;if(!this.closed)return u.delete(this),this.closed=!0,e=this._prepP||i.PROMISE_RESOLVED_VOID,this._onML=null,this._addEL.message=[],e.then(function(){return Promise.all(Array.from(t._uMP))}).then(function(){return Promise.all(t._befC.map(function(e){return e()}))}).then(function(){return t.method.close(t._state)})},get type(){return this.method.type},get isClosed(){return this.closed}}},{"./method-chooser.js":6,"./options.js":11,"./util.js":12}],2:[function(e,t,n){"use strict";var e=e("./index.es5.js"),r=e.BroadcastChannel,e=e.createLeaderElection;window.BroadcastChannel2=r,window.createLeaderElection=e},{"./index.es5.js":3}],3:[function(e,t,n){"use strict";e=e("./index.js");t.exports={BroadcastChannel:e.BroadcastChannel,createLeaderElection:e.createLeaderElection,clearNodeFolder:e.clearNodeFolder,enforceOptions:e.enforceOptions,beLeader:e.beLeader}},{"./index.js":4}],4:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"BroadcastChannel",{enumerable:!0,get:function(){return r.BroadcastChannel}}),Object.defineProperty(n,"OPEN_BROADCAST_CHANNELS",{enumerable:!0,get:function(){return r.OPEN_BROADCAST_CHANNELS}}),Object.defineProperty(n,"beLeader",{enumerable:!0,get:function(){return o.beLeader}}),Object.defineProperty(n,"clearNodeFolder",{enumerable:!0,get:function(){return r.clearNodeFolder}}),Object.defineProperty(n,"createLeaderElection",{enumerable:!0,get:function(){return o.createLeaderElection}}),Object.defineProperty(n,"enforceOptions",{enumerable:!0,get:function(){return r.enforceOptions}});var r=e("./broadcast-channel.js"),o=e("./leader-election.js")},{"./broadcast-channel.js":1,"./leader-election.js":5}],5:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.beLeader=l,n.createLeaderElection=function(e,t){if(e._leaderElector)throw new Error("BroadcastChannel already has a leader-elector");t=function(e,t){e=e||{};(e=JSON.parse(JSON.stringify(e))).fallbackInterval||(e.fallbackInterval=3e3);e.responseTime||(e.responseTime=t.method.averageResponseTime(t.options));return e}(t,e);var n=new o(e,t);return e._befC.push(function(){return n.die()}),e._leaderElector=n};var u=e("./util.js"),r=e("unload"),o=function(e,t){function n(e){"leader"===e.context&&("death"===e.action&&(r.hasLeader=!1),"tell"===e.action&&(r.hasLeader=!0))}var r=this;this.broadcastChannel=e,this._options=t,this.isLeader=!1,this.hasLeader=!1,this.isDead=!1,this.token=(0,u.randomToken)(),this._aplQ=u.PROMISE_RESOLVED_VOID,this._aplQC=0,this._unl=[],this._lstns=[],this._dpL=function(){},this._dpLC=!1;this.broadcastChannel.addEventListener("internal",n),this._lstns.push(n)};function c(e,t){t={context:"leader",action:t,token:e.token};return e.broadcastChannel.postInternal(t)}function l(t){t.isLeader=!0,t.hasLeader=!0;function e(e){"leader"===e.context&&"apply"===e.action&&c(t,"tell"),"leader"!==e.context||"tell"!==e.action||t._dpLC||(t._dpLC=!0,t._dpL(),c(t,"tell"))}var n=(0,r.add)(function(){return t.die()});t._unl.push(n);return t.broadcastChannel.addEventListener("internal",e),t._lstns.push(e),c(t,"tell")}o.prototype={applyOnce:function(s){var a=this;if(this.isLeader)return(0,u.sleep)(0,!0);if(this.isDead)return(0,u.sleep)(0,!1);if(1a.token&&t(),"tell"===e.action&&(t(),a.hasLeader=!0))}var t,n=!1,r=new Promise(function(e){t=function(){n=!0,e()}}),o=[],i=(a.broadcastChannel.addEventListener("internal",e),s?4*a._options.responseTime:a._options.responseTime);return c(a,"apply").then(function(){return Promise.race([(0,u.sleep)(i),r.then(function(){return Promise.reject(new Error)})])}).then(function(){return c(a,"apply")}).then(function(){return Promise.race([(0,u.sleep)(i),r.then(function(){return Promise.reject(new Error)})])}).catch(function(){}).then(function(){return a.broadcastChannel.removeEventListener("internal",e),!n&&l(a).then(function(){return!0})})}return this._aplQC=this._aplQC+1,this._aplQ=this._aplQ.then(e).then(function(){a._aplQC=a._aplQC-1}),this._aplQ.then(function(){return a.isLeader})},awaitLeadership:function(){return this._aLP||(this._aLP=function(o){if(o.isLeader)return u.PROMISE_RESOLVED_VOID;return new Promise(function(e){var t=!1;function n(){t||(t=!0,o.broadcastChannel.removeEventListener("internal",r),e(!0))}o.applyOnce().then(function(){o.isLeader&&n()});(function e(){return(0,u.sleep)(o._options.fallbackInterval).then(function(){if(!o.isDead&&!t)return o.isLeader?void n():o.applyOnce(!0).then(function(){(o.isLeader?n:e)()})})})();var r=function(e){"leader"===e.context&&"death"===e.action&&(o.hasLeader=!1,o.applyOnce().then(function(){o.isLeader&&n()}))};o.broadcastChannel.addEventListener("internal",r),o._lstns.push(r)})}(this)),this._aLP},set onduplicate(e){this._dpL=e},die:function(){var t=this;return this._lstns.forEach(function(e){return t.broadcastChannel.removeEventListener("internal",e)}),this._lstns=[],this._unl.forEach(function(e){return e.remove()}),this._unl=[],this.isLeader&&(this.hasLeader=!1,this.isLeader=!1),this.isDead=!0,c(this,"death")}}},{"./util.js":12,unload:20}],6:[function(e,t,n){"use strict";var r=e("@babel/runtime/helpers/interopRequireDefault"),n=(e("@babel/runtime/helpers/typeof"),Object.defineProperty(n,"__esModule",{value:!0}),n.chooseMethod=function(t){var e=[].concat(t.methods,u).filter(Boolean);if(t.type){if("simulate"===t.type)return s.default;var n=e.find(function(e){return e.type===t.type});if(n)return n;throw new Error("method-type "+t.type+" not found")}t.webWorkerSupport||a.isNode||(e=e.filter(function(e){return"idb"!==e.type}));n=e.find(function(e){return e.canBeUsed()});{if(n)return n;throw new Error("No useable method found in "+JSON.stringify(u.map(function(e){return e.type})))}},r(e("./methods/native.js"))),o=r(e("./methods/indexed-db.js")),i=r(e("./methods/localstorage.js")),s=r(e("./methods/simulate.js")),a=e("./util.js");var u=[n.default,o.default,i.default]},{"./methods/indexed-db.js":7,"./methods/localstorage.js":8,"./methods/native.js":9,"./methods/simulate.js":10,"./util.js":12,"@babel/runtime/helpers/interopRequireDefault":13,"@babel/runtime/helpers/typeof":14}],7:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.TRANSACTION_SETTINGS=void 0,n.averageResponseTime=S,n.canBeUsed=w,n.cleanOldMessages=v,n.close=g,n.commitIndexedDBTransaction=d,n.create=b,n.createDatabase=u,n.default=void 0,n.getAllMessages=function(e){var n=e.transaction(c,"readonly",l),r=n.objectStore(c),o=[];return new Promise(function(t){r.openCursor().onsuccess=function(e){e=e.target.result;e?(o.push(e.value),e.continue()):(d(n),t(o))}})},n.getIdb=a,n.getMessagesHigherThan=h,n.getOldMessages=m,n.microSeconds=void 0,n.onMessage=E,n.postMessage=y,n.removeMessagesById=p,n.type=void 0,n.writeMessage=f;var o=e("../util.js"),i=e("oblivious-set"),s=e("../options.js"),e=o.microSeconds,r=(n.microSeconds=e,"pubkey.broadcast-channel-0-"),c="messages",l={durability:"relaxed"};n.TRANSACTION_SETTINGS=l;function a(){if("undefined"!=typeof indexedDB)return indexedDB;if("undefined"!=typeof window){if(void 0!==window.mozIndexedDB)return window.mozIndexedDB;if(void 0!==window.webkitIndexedDB)return window.webkitIndexedDB;if(void 0!==window.msIndexedDB)return window.msIndexedDB}return!1}function d(e){e.commit&&e.commit()}function u(e){var n=a().open(r+e);return n.onupgradeneeded=function(e){e.target.result.createObjectStore(c,{keyPath:"id",autoIncrement:!0})},new Promise(function(e,t){n.onerror=function(e){return t(e)},n.onsuccess=function(){e(n.result)}})}function f(e,t,n){var r={uuid:t,time:(new Date).getTime(),data:n},o=e.transaction([c],"readwrite",l);return new Promise(function(e,t){o.oncomplete=function(){return e()},o.onerror=function(e){return t(e)},o.objectStore(c).add(r),d(o)})}function h(e,r){var o,i=e.transaction(c,"readonly",l),s=i.objectStore(c),a=[],u=IDBKeyRange.bound(r+1,1/0);return s.getAll?(o=s.getAll(u),new Promise(function(t,n){o.onerror=function(e){return n(e)},o.onsuccess=function(e){t(e.target.result)}})):new Promise(function(t,n){var e=function(){try{return u=IDBKeyRange.bound(r+1,1/0),s.openCursor(u)}catch(e){return s.openCursor()}}();e.onerror=function(e){return n(e)},e.onsuccess=function(e){e=e.target.result;e?e.value.idn.lastCursorId&&(n.lastCursorId=e.id),e}).filter(function(e){return t=n,(e=e).uuid!==t.uuid&&(!t.eMIs.has(e.id)&&!(e.data.time(.*)`)
6 |
7 | const lastSeason = [7123,7121,7119,7117,7116,7114,7113,7112,7111,7110,7108,7107,7106,7105,7104,7102,7100,7098,7096,7094,7091,7089,7087,7085,7083,7078,7076,7075,7074,7073,7068,7066,7064,7062,7060,7058,7057,7055,7054,7053,7049,7048,7047,7046,7045,7043,7042,7041,7040,7039,7038,7037,7036,7035,7034,7033,7032,7031,7030,7029,7028,7027,7026,7025,7024,7023,7022,7021,7020,7019,7018,7017,7016,7015,7014,7013,7012,7011,7010,7009,7008,7005,7004,7003,7002,6999,6997,6996,6995,6994,6993,6992,6991,6990,6989,6987,6986,6985,6984,6983,6981,6980,6979,6977,6976,6974,6972,6971,6969,6968,6967,6966,6964,6963,6962,6961,6960,6958,6957,6955,6953,6951,6950,6949,6948,6947,6945,6944,6943,6942,6938,6937,6935,6934,6933,6932,6931,6930,6928,6927,6924,6923,6922,6921,6920,6917,6916,6915,6913,6911,6906,6904,6903,6902,6901,6900,6899,6898,6897,6896,6895,6894,6893,6892,6891,6890,6889,6888,6887,6886,6885,6884,6883,6882,6881,6880,6879,6878,6877,6876,6872,6871,6870,6869,6868,6866,6865,6864,6863,6862,6861,6860,6859,6858,6857,6856,6855,6854,6853,6852,6851,6850,6849,6848,6847,6846,6845,6844,6843,6842,6841,6840,6839,6838,6837,6835,6834,6833,6832,6831,6830,6829,6828,6827,6826,6825,6824,6823,6822,6821,6699,6697,6695,6694,6691,6686,6684,6682,6680,6678,6672,6670,6667,6666,6664,6659,6657,6655,6653,6651,6623,6622,6620,6618,6616,6614,6612,6610,6608,6607,6605,6604,6603,6602,6601,6600,6599,6598,6597,6596,6593,6592,6591,6590,6589,6588,6587,6586,6585,6584,6583,6582,6581,6580,6579,6578,6577,6576,6575,6574,6571,6570,6569,6568,6567,6565,6564,6562,6561,6557,6556,6555,6554,6553,6552,6551,6550,6549,6548,6547,6545,6544,6543,6542,6541,6540,6539,6538,6537,6536,6535,6534,6533,6532,6531,6530,6529,6528,6525,6524,6523,6520,6517,6514,6513,6512,6511,6510,6509,6508,6507,6506,6505,6504,6503,6502,6501,6500,6499,6498,6497,6496,6495,6493,6491,6486,6485,6484,6483,6482,6481,6480,6479,6478,6477,6473,6472,6471,6470,6469,6468,6467,6466,6465,6464,6463,6462,6461,6460,6459,6456,6455,6454,6453,6452,6451,6450,6449,6448,6447,6446,6445,6444,6443,6442,6441,6440,6439,6438,6437,6434,6433,6432,6431,6429,6426,6425,6424,6423,6422,6420,6419,6418,6417,6416,6414,6413,6412,6411,6410]
8 |
9 | const sfxNames = ["Time Out", "Daily Double", "Final Jeopardy", "Question Open", "Round Over"]
10 | const SFX = sfxNames.map(n => new Audio(`./data/${n}.mp3`))
11 |
12 | let mediaMap = {}
13 |
14 | const fullscreenDiv = document.getElementById('fullscreen')
15 | const startDiv = document.getElementById("start")
16 | const advancedDiv = document.getElementById('advanced')
17 | const incompatibleDiv = document.getElementById('incompatible')
18 |
19 | const startForm = document.getElementById('start_form')
20 | const gameId = document.getElementById('game_id')
21 | const footnoteId = document.getElementById('id_footnote')
22 | const playerInput = document.getElementById('player_names')
23 | const errorText = document.getElementById('error_text')
24 |
25 | const advancedButton = document.getElementById('advanced_button')
26 | const customSelector = document.getElementById('game_file')
27 | const footnoteCustom = document.getElementById('file_footnote')
28 | const customLabel = document.getElementById('custom_label')
29 | const dailyDoubleCheckbox = document.getElementById('dd_checkbox')
30 | const buzzerCheckbox = document.getElementById('buzzer_checkbox')
31 | const timerCheckbox = document.getElementById('time_checkbox')
32 | const playerCheckbox = document.getElementById('player_checkbox')
33 | const timerInput = document.getElementById('time_limit')
34 |
35 | const startButton = document.getElementById('start_button')
36 | const relaunchButton = document.getElementById('relaunch_button')
37 |
38 | const gameDiv = document.getElementById("game")
39 | const questionDiv = document.getElementById("question")
40 | const mediaDiv = document.getElementById("media")
41 |
42 | let currentCell = null
43 | const currentCategory = document.getElementById("question_category")
44 | const currentQuestion = document.getElementById("question_text")
45 | const currentValue = document.getElementById("question_value")
46 |
47 | const dailyDoubleText = document.getElementById("daily_double")
48 |
49 | const scoresDiv = document.getElementById("scores")
50 | const scoreList = document.getElementById('player_scores')
51 |
52 | let timeLimit = null
53 | const pauseDiv = document.getElementById("pause")
54 | let customGame = null
55 |
56 | let divs = [startDiv, gameDiv, questionDiv, mediaDiv, scoresDiv, pauseDiv, incompatibleDiv]
57 | let cid = Date.now() /* todo: localStorage this and link it to the console */
58 | let coid = null
59 |
60 | let consoleWindow = null
61 | let buzzerWindow = null
62 |
63 | let roundKey = null
64 | let hasTiebreaker = false
65 |
66 | let players = {}
67 | let roundData = {}
68 | let roundSettings = {}
69 | let roundNames = []
70 |
71 | function sendMessage(action, params=[]) {
72 | let messageResponse = {
73 | src: "CLIENT",
74 | cid: cid,
75 | coid: coid
76 | }
77 | params.forEach(p => messageResponse[p[0]] = p[1])
78 | bc.postMessage({
79 | action: action,
80 | response: messageResponse
81 | })
82 | }
83 | const sendStartMessage = () => sendMessage("START_GAME", [
84 | ['players', players],
85 | ['board', Object.entries(roundData).map(([k, v]) => {return {[k]: v.board}})],
86 | ['limit', timeLimit],
87 | ["roundKey", roundKey],
88 | ["settings", roundSettings],
89 | // on console relaunch, apply proper styling to seen questions
90 | ["seen", Array.from(document.querySelectorAll(".question_cell[data-seen='true']")).filter(c => c.dataset.question != 'undefined').map(c => c.dataset.cell)]
91 | ])
92 |
93 | let pulse = null //setTimeout used to communicate data from client -> console
94 | let hasLoaded = false
95 |
96 | let debug = true
97 |
98 | window.addEventListener('beforeunload', (event) => {
99 | if(buzzerWindow) buzzerWindow.close()
100 | sendMessage("CLIENT_CLOSE")
101 | })
102 |
103 | function heartbeat() {
104 | if(!coid) sendMessage("LINK_CLIENT")
105 | return setInterval(() => {
106 | if(!coid) sendMessage("LINK_CLIENT")
107 | else clearInterval(pulse)
108 | }, 1000)
109 | }
110 |
111 | const randInt = (max, min, incl=false) => Math.floor(Math.random()*(max - min)) + min + incl
112 | const show = (elem, as='block') => elem.style.display = as
113 | const hide = (elem, useNone=true) => elem.style.display = (useNone) ? ('none') : ('hidden')
114 |
115 | bc.onmessage = function(msg) {
116 | const action = msg.action
117 | const data = msg.response
118 | if(data.src === "CONSOLE" && data.cid === cid) {
119 | switch(action) {
120 | case "LINK_CONSOLE":
121 | console.log("Loading game...")
122 | coid = data.coid
123 | if(hasLoaded) setState(gameDiv)
124 | sendStartMessage()
125 | break
126 | case "CONSOLE_CLOSE":
127 | if(data.coid === coid) {
128 | coid = null;
129 | if(activeState() != startDiv) {
130 | hideMedia();
131 | setState(pauseDiv)
132 | }
133 | }
134 | break
135 | case "SHOW_SCORES":
136 | if(data.coid === coid) {
137 | updateScoreList()
138 | setState(scoresDiv)
139 | }
140 | break
141 | case "SHOW_BOARD":
142 | if(data.coid === coid) setState(gameDiv)
143 | break
144 | case "UPDATE_PLAYERS":
145 | if(data.coid === coid) {
146 | players = data.players
147 | updateScoreList()
148 | }
149 | break
150 | case "OPEN_QUESTION":
151 | if(data.coid === coid) {
152 | const isTrue = v => ['true', true].includes(v)
153 | if([data.dd, data.final].some(isTrue)) {
154 | dailyDoubleText.textContent = isTrue(data.dd) ? ("DAILY DOUBLE") : ("FINAL JEOPARDY")
155 | dailyDoubleText.style.display = 'block'
156 | currentQuestion.style.display = 'none'
157 | currentValue.style.display = 'none'
158 | } else {
159 | dailyDoubleText.style.display = 'none'
160 | currentQuestion.style.display = 'block'
161 | currentValue.style.display = 'block'
162 | }
163 | setState(questionDiv)
164 | currentCell.style.color = 'grey'
165 | currentCell.dataset.seen = 'true'
166 | }
167 | break
168 | case "SET_VALUE":
169 | if(data.coid === coid) {
170 | currentValue.textContent = data.label;
171 | }
172 | break
173 | case "CELL_CLICKED":
174 | if(data.coid == coid) {
175 | const relevantQuestion = document.querySelector(`.question_cell[data-cell='${data.cell}']`)
176 | if(relevantQuestion) showQuestion(relevantQuestion)
177 | }
178 | break
179 | case "SHOW_QUESTION":
180 | if(data.coid === coid) {
181 | currentQuestion.style.display = 'block'
182 | currentValue.style.display = 'block'
183 | }
184 | break
185 | case "CLOSE_QUESTION":
186 | if(data.coid === coid) {
187 | hideMedia();
188 | setState(gameDiv)
189 | }
190 | break
191 | case "PROGRESS_ROUND":
192 | if(data.coid === coid) progressRound()
193 | break
194 | case "SET_ROUND":
195 | if(data.coid === coid) setRound(data.roundKey);
196 | break;
197 | case "REGRESS_ROUND":
198 | if(data.coid === coid) regressRound()
199 | break
200 | case "PLAY_SFX":
201 | case "PAUSE_SFX":
202 | if(data.coid === coid) playSFX(data.sfx) //null if pause
203 | break
204 |
205 | case "PLAY_MEDIA":
206 | if(data.coid === coid) {
207 | showMedia();
208 | }
209 | break;
210 | case "PAUSE_MEDIA":
211 | if(data.coid === coid) {
212 | hideMedia();
213 | }
214 | break;
215 | case "RESTART":
216 | if(data.coid === coid) setup()
217 | break
218 | case "OPEN_BUZZERS":
219 | if(data.coid === coid) {
220 | if(!buzzerWindow.closed) {
221 | if(buzzerWindow.location === buzzUrl) buzzerWindow.location.reload()
222 | else {
223 | buzzerWindow.location.replace(buzzUrl)
224 | }
225 | } else {
226 | buzzerWindow = window.open(buzzUrl, `${cid}_BUZZERS`, 'toolbar=0,location=0,menubar=0')
227 | }
228 | }
229 | break;
230 | default:
231 | console.log("Received unimplemented action: ", msg.data)
232 | break
233 | }
234 | }
235 | }
236 |
237 | function shouldTiebreaker() {
238 | let pv = Object.values(players)
239 | return hasTiebreaker && !pv.map(ps => pv.indexOf(ps) === pv.lastIndexOf(ps)).every(s => s)
240 | }
241 |
242 | function setRound(rk) {
243 | for(let round of roundNames) {
244 | if(round !== rk) document.querySelector(`.game_table.${round}`).style.display = 'none';
245 | else {
246 | document.querySelector(`.game_table.${rk}`).style.display = 'table';
247 | roundKey = round;
248 | sendMessage("SET_ROUND", [["roundKey", roundKey]])
249 | }
250 | }
251 | }
252 |
253 | function progressRound() {
254 | for(let i = 0; i < roundNames.length; i++) {
255 | if(roundNames[i] === roundKey) {
256 | if(roundData[roundKey].settings.mode === 'final' || i === roundNames.length - 1) {
257 | show(document.getElementById('final_text'), 'inline')
258 | updateScoreList()
259 | // PROGRESS_ROUND message is send with SHOW_BOARD, so circumvent by requesting score view
260 | sendMessage("REQUEST_SCORES")
261 | break;
262 | } else {
263 | document.querySelector(`.game_table.${roundKey}`).style.display = 'none';
264 | roundKey = roundNames[i+1];
265 | document.querySelector(`.game_table.${roundKey}`).style.display = 'table';
266 | console.log(`Progressed to ${roundKey}`)
267 | sendMessage("SET_ROUND", [["roundKey", roundKey]])
268 | break;
269 | }
270 | }
271 | }
272 | }
273 |
274 | function regressRound() {
275 | for(let i = roundNames.length - 1; i > 0; i--) {
276 | if(roundNames[i] === roundKey) {
277 | document.querySelector(`.game_table.${roundKey}`).style.display = 'none';
278 | roundKey = roundNames[i - 1];
279 | document.querySelector(`.game_table.${roundKey}`).style.display = 'table';
280 |
281 | console.log(`Regresed to ${roundKey}`)
282 | sendMessage("SET_ROUND", [["roundKey", roundKey]])
283 | break
284 | }
285 | }
286 | }
287 |
288 | function cleanup(elem) {
289 | while(elem.lastChild) elem.removeChild(elem.lastChild);
290 | }
291 |
292 | function updateScoreList() {
293 | while(scoreList.lastChild) {
294 | scoreList.removeChild(scoreList.lastChild)
295 | }
296 | Object.entries(players).sort((a, b) => b[1] - a[1]).forEach(pe => {
297 | let pl = document.createElement('li')
298 | pl.textContent = `${pe[0]}: ${pe[1]}`
299 | scoreList.appendChild(pl)
300 | })
301 | }
302 |
303 | function setup() {
304 | hasLoaded = false
305 | roundData = {}
306 | customGame = null
307 |
308 | if(localStorage.getItem('showAdvanced') === 'true') {
309 | advancedButton.textContent = 'Hide Advanced'
310 | advancedDiv.style.display = 'block'
311 | }
312 |
313 | let playedList = localStorage.getItem('playedList')
314 | let unusedIds;
315 |
316 | if(playedList) {
317 | unusedIds = lastSeason.filter(lsid => !JSON.parse(playedList).map(gid => parseInt(gid)).includes(lsid))
318 | }
319 | else unusedIds = lastSeason
320 |
321 | gameId.value = unusedIds[randInt(0, unusedIds.length, false)]
322 | footnoteId.setAttribute('href', `http://www.j-archive.com/showgame.php?game_id=${gameId.value}`)
323 |
324 | customSelector.value = null
325 | customLabel.textContent = "Select File..."
326 |
327 | if(startButton.getAttribute('disabled')) startButton.removeAttribute('disabled')
328 | if(bc) {
329 | setState(startDiv)
330 | } else {
331 | setState(incompatibleDiv)
332 | }
333 | }
334 |
335 | function launchConsole() {
336 | let consoleLoc = new String(window.location)
337 | if(consoleLoc.includes('jeo.zamiton.com')) {
338 | consoleLoc = 'https://jeo.zamiton.com/console'
339 | } else if(consoleLoc.includes('nathansbud.github.io/Jeopardizer')) {
340 | consoleLoc = 'https://nathansbud.github.io/Jeopardizer/console.html'
341 | } else {
342 | consoleLoc = './console.html'
343 | }
344 |
345 | consoleWindow = window.open(consoleLoc, `${cid}_CONSOLE`, 'toolbar=0,location=0,menubar=0')
346 | if(buzzerCheckbox.checked && (!buzzerWindow || buzzerWindow.closed)) {
347 | buzzerWindow = window.open(buzzUrl, `${cid}_BUZZERS`, 'toolbar=0,location=0,menubar=0')
348 | }
349 |
350 | self.focus()
351 | }
352 |
353 | window.onload = function() {
354 | setup()
355 | relaunchButton.addEventListener('click', () => {
356 | pulse = heartbeat()
357 | launchConsole()
358 | })
359 | startButton.addEventListener('click', function() {
360 | players = {}
361 | timeLimit = (timerCheckbox.checked && timerInput.value >= timerInput.min) ? (timerInput.value) : (null)
362 | let queryId = gameId.value
363 |
364 | let playerNames = (playerInput.value) ?
365 | (playerInput.value.split(",").map(pn => pn.trim())) :
366 | (!playerCheckbox.checked ? [] : playerInput.value);
367 |
368 | if(playerNames || !playerCheckbox.checked) {
369 | startButton.setAttribute('disabled', true)
370 | playerNames.forEach(pn => players[pn] = 0)
371 | if(customGame) {
372 | startGame(customGame)
373 | } else if(queryId && queryId >= gameId.min && queryId <= gameId.max) {
374 | getGame(queryId).then((value) => {
375 | startGame(value)
376 | if(localStorage.getItem('playedList')) {
377 | let pl = JSON.parse(localStorage.getItem('playedList'))
378 | pl.push(queryId)
379 | localStorage.setItem('playedList', JSON.stringify(pl))
380 | } else {
381 | localStorage.setItem('playedList', JSON.stringify([queryId]))
382 | }
383 | }).catch((e) => {
384 | setup()
385 | errorText.style.display = 'block'
386 | errorText.textContent = "J-Archive is offline! Try again later, or choose a custom game instead.";
387 |
388 | const storedId = queryId
389 | gameId.value = storedId
390 | })
391 |
392 | } else {
393 | startButton.disabled = false
394 | return
395 | }
396 | }
397 | })
398 | }
399 |
400 |
401 | function startGame(gameObj) {
402 | const loadGameContainer = () => {
403 | try {
404 | return loadGame(gameObj)
405 | } catch(e) {
406 | return {passed: false, reason: "Unknown reason; if custom, try editing your game, otherwise try again later!"}
407 | }
408 | }
409 |
410 | const {
411 | data, passed, reason, settings
412 | } = loadGameContainer()
413 |
414 | if(!passed) {
415 | const errorMsg = `Failed to load game: ${reason ?? 'unknown reason'}`
416 | errorText.textContent = errorMsg;
417 | errorText.style.display = 'block';
418 | return;
419 | } else {
420 | errorText.style.display = 'none';
421 | }
422 |
423 | roundData = data;
424 | roundNames = Object.keys(data);
425 |
426 | Object.entries(data).forEach(([roundName, roundInfo]) => {
427 | const { table } = roundInfo;
428 | gameDiv.appendChild(table);
429 | table.style.display = (roundName != roundKey) ? 'none' : 'table';
430 | })
431 |
432 | roundSettings = settings;
433 |
434 | if(!coid) {
435 | launchConsole()
436 | pulse = heartbeat()
437 | }
438 | else sendStartMessage()
439 |
440 | console.log("Sent linking message...")
441 |
442 | if(coid) setState(gameDiv)
443 | hasLoaded = true
444 | }
445 |
446 | function setState(div) {
447 | divs.forEach(d => {
448 | if(d != div) d.style.display = 'none'
449 | else d.style.display = 'block'
450 | })
451 | }
452 |
453 | function activeState() {
454 | for(let d of divs) {
455 | if(d.style.display === 'block') return d;
456 | }
457 | }
458 |
459 | customSelector.addEventListener('change', () => {
460 | console.log("Attempting to load custom game...")
461 | loadCustom()
462 | })
463 | advancedButton.addEventListener('click', () => {
464 | if(advancedDiv.style.display === 'block') {
465 | advancedDiv.style.display = 'none'
466 | advancedButton.textContent = 'Show Advanced'
467 | localStorage.setItem('showAdvanced', false)
468 | } else {
469 | advancedDiv.style.display = 'block'
470 | advancedButton.textContent = 'Hide Advanced'
471 | localStorage.setItem('showAdvanced', true)
472 | }
473 | })
474 |
475 | function loadCustom() {
476 | const JSONReader = new FileReader()
477 | JSONReader.onload = function(e) {
478 | try {
479 | customGame = JSON.parse(e.target.result)
480 | customLabel.textContent = customSelector.files[0].name
481 | } catch(e) {
482 | console.log(e)
483 | customGame = null
484 | customLabel.textContent = 'Invalid file!'
485 | }
486 | }
487 | JSONReader.readAsText(customSelector.files[0])
488 | }
489 |
490 | function loadGame(config) {
491 | while(gameDiv.lastChild) {
492 | gameDiv.removeChild(gameDiv.lastChild)
493 | }
494 |
495 | const parsedData = {}
496 | const parsedRounds = config.rounds;
497 |
498 | if(!parsedRounds) {
499 | return {passed: false, reason: "Custom games must have at least 1 round!"};
500 | }
501 |
502 | let firstRound = true;
503 | for(let [roundNum, round] of parsedRounds.entries()) {
504 | const {
505 | name,
506 | multiplier,
507 | categories,
508 | mode,
509 | dds
510 | } = round;
511 |
512 | const roundName = ("" + (name ?? `round-${roundNum + 1}`)).trim().toLowerCase()
513 | if(roundName.match(/[^A-Za-z0-9\-_]/)) {
514 | return {passed: false, reason: "Custom rounds may only contain characters: alphanumeric characters, hyphens (-), and underscores (_)!"}
515 | }
516 |
517 | if(firstRound) {
518 | roundKey = roundName;
519 | firstRound = false;
520 | }
521 |
522 | if(!categories) return {passed: false, reason: "Custom rounds must have at least 1 category!"}
523 | const numCategories = categories.length;
524 | const requiredRows = categories.reduce((acc, curr) => {
525 | const catLength = curr.clues.length;
526 | if(catLength >= acc) {
527 | return catLength;
528 | }
529 | return acc;
530 | }, 0)
531 | const roundBoard = [...Array(requiredRows + 1)].map(_ => Array(numCategories))
532 | const validDds = new Set(getRange(numCategories * requiredRows));
533 |
534 | for(let [column, cat] of categories.entries()) {
535 | const { clues, comment, category } = cat;
536 |
537 | roundBoard[0][column] = {category: category, comment: comment}
538 | for(let row = 0; row < requiredRows; row++) {
539 | const ques = clues[row] ?? {};
540 | const { question, answer, value, dd, media, label } = ques;
541 | const baseValue = 200 * (row + 1);
542 | const questionKey = `${roundName}-${row}-${column}`;
543 |
544 | if(!answer) validDds.delete(row * numCategories + column)
545 | if(media) {
546 | const { type, path, metadata } = media;
547 | let mediaItem;
548 | switch(type) {
549 | case "audio":
550 | mediaItem = new Audio(path);
551 | mediaItem.currentTime = metadata.start ?? 0;
552 | if(metadata.end) {
553 | mediaItem.addEventListener('timeupdate', () => {
554 | if(mediaItem.currentTime >= metadata.end) {
555 | mediaItem.pause();
556 | mediaItem.currentTime = metadata.start ?? 0;
557 | }
558 | })
559 | }
560 | break;
561 | case "image":
562 | mediaItem = new Image();
563 | if(metadata.width) mediaItem.width = metadata.width;
564 | if(metadata.height) mediaItem.height = metadata.height;
565 | mediaItem.src = path;
566 | break;
567 | case "video":
568 | mediaItem = document.createElement("video");
569 | mediaItem.currentTime = metadata.start ?? 0;
570 | mediaItem.src = path;
571 | mediaItem.controls = false;
572 |
573 | if(metadata.width) mediaItem.width = metadata.width;
574 | if(metadata.height) mediaItem.height = metadata.height;
575 |
576 | if(metadata.end) {
577 | mediaItem.addEventListener('timeupdate', () => {
578 | if(mediaItem.currentTime >= metadata.end) {
579 | mediaItem.pause();
580 | mediaItem.currentTime = metadata.start ?? 0;
581 | }
582 | })
583 | }
584 | break;
585 | default:
586 | console.log(`Attempted to load unsupported media type: ${type}`)
587 | break;
588 | }
589 |
590 | if(mediaItem) {
591 | mediaMap[questionKey] = {
592 | media: mediaItem,
593 | metadata: metadata,
594 | type: type,
595 | }
596 | }
597 | }
598 |
599 | const computedValue = value ?? (baseValue * (multiplier ?? 1));
600 |
601 | roundBoard[row + 1][column] = {
602 | cell: `${roundName}-${row}-${column}`,
603 | idx: row * numCategories + column,
604 | question: question ?? "",
605 | answer: answer ?? "",
606 | value: computedValue,
607 | label: label ?? `$${computedValue}`,
608 | category: category,
609 | comment: comment,
610 | dd: Boolean(dd ?? false),
611 | final: ques.final ?? cat.final ?? round.final,
612 | client: true,
613 | media: !!media
614 | }
615 | }
616 | }
617 |
618 | const ddIndices = shuffle(Array.from(validDds)).slice(0, Math.min(Math.abs(dds ?? 0), validDds.size))
619 | ddIndices.forEach(dd => {
620 | roundBoard[1 + Math.floor(dd / numCategories)][dd % numCategories].dd = true;
621 | })
622 |
623 | const roundTable = document.createElement('table');
624 | roundTable.classList.add("game_table", roundName);
625 |
626 | const tableHeader = document.createElement('tr');
627 | roundBoard[0].forEach(c => {
628 | const header = document.createElement('th');
629 | header.textContent = c.category;
630 | header.dataset['comment'] = c.comment;
631 | tableHeader.appendChild(header);
632 | })
633 |
634 | roundTable.appendChild(tableHeader);
635 | roundBoard.slice(1).forEach(row => {
636 | const newRow = document.createElement('tr');
637 | row.forEach(q => {
638 | const cell = document.createElement("td");
639 | cell.classList.add('question_cell');
640 | cell.textContent = q.label;
641 | cell.setAttribute('disabled', !!!q.answer);
642 | cell.addEventListener('click', function() {
643 | if(this.getAttribute('disabled').includes('false')) {
644 | showQuestion(this)
645 | this.dataset.seen = "true"
646 | }
647 | })
648 |
649 | Object.entries(q).forEach(([k, v]) => {cell.dataset[k] = v});
650 | newRow.appendChild(cell);
651 | })
652 | roundTable.appendChild(newRow);
653 | })
654 |
655 | parsedData[roundName] = {
656 | board: roundBoard,
657 | table: roundTable,
658 | settings: {
659 | mode: mode
660 | }
661 | };
662 | }
663 |
664 | return {
665 | passed: true,
666 | data: parsedData,
667 | settings: {
668 | "ddEnabled": config.ddEnabled ?? dailyDoubleCheckbox.checked,
669 | }
670 | }
671 | }
672 |
673 | function showQuestion(cell) {
674 | currentCell = cell
675 |
676 | currentCategory.textContent = cell.getAttribute('data-category')
677 | currentQuestion.textContent = cell.getAttribute('data-question')
678 | currentValue.textContent = cell.getAttribute('data-label')
679 |
680 | sendMessage("LOAD_QUESTION", Object.entries(cell.dataset))
681 | }
682 |
683 | function extractAnswer(clue) {
684 | return clue?.querySelector('.correct_response')?.textContent;
685 | }
686 |
687 | function getCategories(tables) {
688 | return tables.map(r => Array.from(r.getElementsByClassName("category")))
689 | .map(c => c.map(function(cn) {
690 | return {
691 | "category":cn.querySelector(".category_name").textContent.trim(),
692 | "comment":cn.querySelector('.category_comments').textContent.trim()
693 | }
694 | }))
695 | }
696 |
697 | async function getGame(gid) {
698 | const response = await fetch(`${corsUrl}https://www.j-archive.com/showgame.php?game_id=${gid}`)
699 | const pageContent = new DOMParser().parseFromString(await response.text(), 'text/html')
700 |
701 | if(pageContent.querySelector('.error')) throw Error("J-Archive error, game could not be loaded")
702 |
703 | const header = pageContent.getElementById("game_title").textContent
704 | const gameDate = new Date(header.split("-")[1]) //something like "Friday, October 18, 2019"
705 | const categorySet = {}
706 |
707 | let rounds = Array.from(pageContent.getElementsByClassName("round")).concat(Array.from(pageContent.getElementsByClassName("final_round")))
708 | let categories = getCategories(rounds)
709 | let questions = rounds.map(r => Array.from(r.getElementsByClassName("clue")))
710 | .map(c => c.map(function(cn) {
711 | return {
712 | "question": cn.querySelector('.clue_text')?.textContent.trim(),
713 | "answer": extractAnswer(cn)
714 | }
715 | }))
716 |
717 | let roundSet = []
718 | for(let [i, r] of categories.entries()) {
719 | for(let [j, cat] of Array.from(r).entries()) {
720 | if(i < 2) cat['clues'] = questions[i].filter((_, idx) => ((idx - j) % 6) === 0)
721 | else cat['clues'] = [{question: questions[i][0]['question'], answer: questions[i][0]['answer']}]
722 | }
723 | roundSet.push(r)
724 | }
725 |
726 | return {
727 | "rounds": [
728 | {
729 | "name": "single_jeopardy",
730 | "dds": 1,
731 | "categories": roundSet[0]
732 | },
733 | {
734 | "name": "double_jeopardy",
735 | "dds": 2,
736 | "multiplier": 2,
737 | "categories": roundSet[1]
738 | },
739 | {
740 | "name": "final_jeopardy",
741 | "categories": roundSet[2],
742 | "final": true
743 | }
744 | ]
745 | }
746 | }
747 |
748 | footnoteId.addEventListener('change', function() {
749 | footnoteId.setAttribute('href', `http://www.j-archive.com/showgame.php?game_id=${gameId.value}`)
750 | })
751 |
752 | function playSFX(sf) {
753 | for(let s of SFX) {
754 | if(!s.paused) {
755 | s.pause()
756 | s.currentTime = 0
757 | }
758 | }
759 | if(sf) SFX[sfxNames.indexOf(sf)].play()
760 | }
761 |
762 | function showMedia() {
763 | const mediaEntry = mediaMap[currentCell?.dataset.cell];
764 | if(mediaEntry) {
765 | const { media, metadata, type } = mediaEntry;
766 | switch(type) {
767 | case "audio":
768 | media.play();
769 | break;
770 | case "image":
771 | cleanup(mediaDiv);
772 | mediaDiv.appendChild(media);
773 | setState(mediaDiv);
774 | break;
775 | case "video":
776 | cleanup(mediaDiv);
777 | mediaDiv.appendChild(media);
778 | setTimeout(() => media.play(), 500);
779 | setState(mediaDiv);
780 | break;
781 | default:
782 | console.log("Tried to display unsupported media type!")
783 | break;
784 | }
785 | }
786 | }
787 |
788 | function hideMedia() {
789 | const mediaEntry = mediaMap[currentCell?.dataset.cell];
790 | if(mediaEntry) {
791 | const { media, metadata, type } = mediaEntry;
792 | switch(type) {
793 | case "audio":
794 | media.pause();
795 | media.currentTime = metadata.start ?? 0;
796 | break;
797 | case "image":
798 | setState(questionDiv);
799 | break;
800 | case "video":
801 | media.pause();
802 | media.currentTime = metadata.start ?? 0;
803 | setState(questionDiv);
804 | break;
805 | default:
806 | console.log("Tried to display unsupported media type!")
807 | break;
808 | }
809 | }
810 | }
811 |
812 |
813 | function getRange(max, min=0) {
814 | if(min < 0) min = 0
815 | return [...Array(max).keys()].slice(min)
816 | }
817 |
818 | //https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
819 | function shuffle(arr) {
820 | for (let i = arr.length - 1; i > 0; i--) {
821 | let j = Math.floor(Math.random() * (i + 1))
822 | ;[arr[i], arr[j]] = [arr[j], arr[i]]
823 | }
824 | return arr
825 | }
826 |
--------------------------------------------------------------------------------
/app/src/Game.java:
--------------------------------------------------------------------------------
1 | import processing.core.PApplet;
2 | import processing.core.PFont;
3 |
4 | import processing.event.KeyEvent;
5 | import processing.event.MouseEvent;
6 |
7 | import org.json.simple.*;
8 | import org.json.simple.parser.JSONParser;
9 | import org.json.simple.parser.ParseException;
10 |
11 | import ddf.minim.*;
12 |
13 | import java.io.File;
14 | import java.io.FileReader;
15 | import java.io.BufferedReader;
16 | import java.io.InputStreamReader;
17 | import java.io.IOException;
18 |
19 | import java.util.ArrayList;
20 | import java.util.LinkedList;
21 | import java.util.Queue;
22 | import java.util.Iterator;
23 | import java.util.Collections;
24 | import java.util.Timer;
25 | import java.util.TimerTask;
26 | import java.util.concurrent.ThreadLocalRandom;
27 |
28 | /*-------------------------------------------------------------------------------------------------*\
29 |
30 | TO DO ZONE:
31 | - Settings Class
32 | - More flexibility
33 | - More "releasable"
34 | - Player Records
35 | - Log "created" players to player data file
36 | - Differentiate by full name (i.e. John Smith) but display "John" as player name (Jeopardy only does first name)
37 | - Present select screen to setup game's players, if none exist present option to ADD player
38 | - Log wins for a player, for stuff like "returning champion"
39 | - Screen Class:
40 | - Rework "round" to be contained by a greater screen class
41 | - Allows for more flexibility to have things removed from "game" logic
42 | - Menu systems, player select, settings select, select scrape game...abstract pure code logic -> GUI
43 | - ScrollableScreen Class:
44 | - Screen-type which inherits from Screen but is scrollable
45 | - Might require abstract "JObject" parent class or interface which things inherit from, which contains X/Y/ScrollableArea
46 | - Allows for things like selectable list
47 | \*------------------------------------------------------------------------------------------------*/
48 |
49 | public class Game extends PApplet {
50 | public enum GameState {
51 | ROUND(),
52 | QUESTION(),
53 | SCORES(),
54 | SETTINGS(),
55 | PLAYER_SETUP()
56 | }
57 |
58 | private static Game app = new Game();
59 | private static Console console = new Console();
60 |
61 | private static GameState gameState = GameState.ROUND;
62 |
63 | private static Round first = new Round(Round.RoundType.SINGLE);
64 | private static Round second = new Round(Round.RoundType.DOUBLE);
65 | private static Round third = new Round(Round.RoundType.FINAL);
66 |
67 | private static ArrayList customRounds = new ArrayList<>(); //To be used for custom rounds
68 | private static Queue progressionPath = new LinkedList<>(); //Used to set path of rounds; 1st->2nd-3rd is classic, but opens up to new possibilities
69 |
70 |
71 | private static ArrayList playerSet = new ArrayList();
72 | private static ArrayList players = new ArrayList();
73 |
74 | private static String[] playerNames = {
75 | "Player 1", "Player 2", "Player 3"
76 | };
77 |
78 | private static Timer timer = new Timer();
79 | private static boolean timerState = false;
80 |
81 | private static Minim minim;
82 | private static AudioPlayer tracks[] = new AudioPlayer[4]; //Stores Jeopardy SFXs
83 |
84 | private static String wager = "";
85 | private static int filterYear = 2008;
86 | private static int upperFilterYear = 2019;
87 |
88 | private static boolean useFilter = true;
89 | private static boolean isCustom = false;
90 | private static boolean isScraped = false;
91 | private static boolean musicEnabled = true;
92 |
93 | private static boolean useCustomFonts = false;
94 | private static PFont qfont = null; //Korinna, used for custom question font
95 | private static PFont cfont = null; //Helvetica Inserat, used for custom category font
96 | private static PFont mfont = null; //Swiss 911, used for price value font
97 |
98 | private static boolean testMode = false;
99 |
100 | private static float vOffset = 0.01f;
101 | private static float maxOffset = 0.01f; //Don't want divide by 0s
102 |
103 | //Unfinished Zone
104 | // private static ScrollableScreen settingSelect = new ScrollableScreen();
105 |
106 | //Game Setter/Getter Zone//
107 | public static GameState getGameState() {
108 | return gameState;
109 | }
110 | public static ArrayList getPlayers() {
111 | return players;
112 | }
113 | public static String getWager() {
114 | return wager;
115 | }
116 | public static PApplet getGUI() {
117 | return app;
118 | }
119 | public static Minim getMinim() {
120 | return minim;
121 | }
122 | public static boolean getTimerState() {
123 | return timerState;
124 | }
125 |
126 | public static PFont getQuestionFont() {
127 | return qfont;
128 | }
129 | public static PFont getCategoryFont() {
130 | return cfont;
131 | }
132 | public static PFont getMoneyFont() {
133 | return mfont;
134 | }
135 | public static boolean isUseCustomFonts() {
136 | return useCustomFonts;
137 | }
138 |
139 | @Override public void settings() {
140 | fullScreen(2);
141 | }
142 |
143 | @Override public void setup() {
144 | // minim = new Minim(app);
145 | minim = new Minim(app);
146 |
147 | Question.setConstants(app);
148 | Category.setGui(app);
149 | ScrollableScreen.setGui(app);
150 |
151 |
152 | if(!isCustom) {
153 | first.setup();
154 | second.setup();
155 | } else {
156 | first.setup();
157 | }
158 |
159 | if(musicEnabled) {
160 | tracks[0] = minim.loadFile("data" + File.separator + "audio" + File.separator + "Time Out.mp3");
161 | tracks[1] = minim.loadFile("data" + File.separator + "audio" + File.separator + "Daily Double.mp3");
162 | tracks[2] = minim.loadFile("data" + File.separator + "audio" + File.separator + "Final Jeopardy.mp3");
163 | tracks[3] = minim.loadFile("data" + File.separator + "audio" + File.separator + "Question Open.mp3");
164 | }
165 |
166 | for(Round r : progressionPath) {
167 | for(Category c : r.getCategories()) {
168 | for(Question q : c.getQuestions()) {
169 | if(q.hasMedia()) {
170 | q.getMedia().load();
171 | }
172 | }
173 | }
174 | }
175 |
176 | Round.setCurrentRound(progressionPath.poll());
177 | String pathToFont = "data" + File.separator + "fonts";
178 | if(useCustomFonts) {
179 | for (File f : new File(pathToFont).listFiles()) {
180 | String fontName = f.getName().substring(0, f.getName().indexOf("."));
181 | switch (fontName) {
182 | case "Question":
183 | qfont = createFont(f.getAbsolutePath(), 12, true);
184 | break;
185 | case "Category":
186 | cfont = createFont(f.getAbsolutePath(), 12, true);
187 | break;
188 | case "Values":
189 | mfont = createFont(f.getAbsolutePath(), 12, true);
190 | break;
191 | }
192 | }
193 | }
194 | }
195 |
196 | @Override public void draw() {
197 | switch(gameState) {
198 | case ROUND:
199 | background(0);
200 | Round.getCurrentRound().draw();
201 | break;
202 | case QUESTION:
203 | Question.getSelected().draw();
204 | break;
205 | case SCORES:
206 | background(PApplet.unhex(JConstants.JEOPARDY_BLUE));
207 | fill(255);
208 |
209 | if (useCustomFonts) {
210 | textFont(cfont);
211 | textSize(60);
212 | } else {
213 | textSize(35);
214 | }
215 |
216 | for (int i = 0; i < players.size(); i++) {
217 | text(players.get(i).getName() + ": $" + (players.get(i).getScore()), width / 3.0f, height / 8.0f * (i + 1));
218 | }
219 | break;
220 | case PLAYER_SETUP:
221 | background(PApplet.unhex(JConstants.JEOPARDY_BLUE));
222 | textSize(35);
223 | for (int i = 0; i < playerSet.size(); i++) {
224 | fill(50);
225 | //idk why it has to be a % but screw getting textHeight LUL
226 | //this needs to be -> button class, with text, hover, x/y...
227 | rect(width/3.0f, height/8.0f * (i + 1)+vOffset - 0.75f*(textAscent() + textDescent()) , textWidth(playerSet.get(i).getName() + " (Wins: " + playerSet.get(i).getWins() + ")"), textAscent() + textDescent());
228 | fill(255);
229 | text(playerSet.get(i).getName() + " (Wins: " + playerSet.get(i).getWins() + ")", width / 3.0f, height / 8.0f * (i + 1) + vOffset);
230 | }
231 | System.out.println(vOffset);
232 | System.out.println(maxOffset);
233 | rect(width - 10, 0f - (vOffset/maxOffset)*(height - 20), 10, 20, 10);
234 | break;
235 | }
236 | }
237 |
238 | @Override public void mouseClicked() {
239 | switch(gameState) {
240 | case ROUND:
241 | if(Question.getSelected() == null) {
242 | for (Category c : Round.getCurrentRound().getCategories()) {
243 | for (Question q : c.getQuestions()) {
244 | if (mouseX > q.getX() && mouseX < (q.getX() + Question.getWidth()) && mouseY > q.getY() && mouseY < q.getY() + Question.getHeight() && !q.isAnswered()) {
245 | for (Player p : Game.getPlayers()) {
246 | System.out.println(p.getName() + ": " + p.getScore());
247 | }
248 |
249 | if (q.isDailyDouble()) {
250 | // tracks[1].play();
251 | }
252 | q.setAnswered(true);
253 | Question.setSelected(q);
254 | gameState = GameState.QUESTION;
255 | return;
256 | }
257 | }
258 | }
259 | }
260 | break;
261 | case PLAYER_SETUP:
262 | System.out.println("Clicked on player screen");
263 | break;
264 | default:
265 | break;
266 | }
267 | }
268 |
269 | @Override public void keyPressed(KeyEvent event) {
270 | // System.out.println(gameState);
271 | // System.out.println(event.getKeyCode());
272 | switch(gameState) {
273 | case ROUND:
274 | switch (event.getKeyCode()) { //To-do: make this less hardcoded for custom categories
275 | case 16: //LShift
276 | gameState = GameState.SCORES;
277 | break;
278 | case 192:
279 | progressRound();
280 | break;
281 | }
282 | break;
283 | case SCORES:
284 | switch(event.getKeyCode()) {
285 | case 16:
286 | gameState = GameState.ROUND;
287 | break;
288 | }
289 | break;
290 | case QUESTION:
291 | switch(event.getKeyCode()) {
292 | case 8: //DELETE, HANDLE INCORRECT RESPONSE
293 | if(Player.getActive() != null) {
294 | Player.getActive().changeScore(-Question.getSelected().getValue());
295 | System.out.println(Player.getActive().getScore());
296 | }
297 | wager = "";
298 | break;
299 | case 9: //TAB
300 | if(Question.getSelected().hasMedia()) {
301 | if(Question.getSelected().getMedia().getType() == Media.MediaType.AUDIO) {
302 | ((AudioPlayer)Question.getSelected().getMedia().getMedia()).pause();
303 | }
304 | }
305 | Question.setSelected(null);
306 | if(Round.getCurrentRound().getRoundType() != Round.RoundType.FINAL) gameState = GameState.ROUND;
307 | else gameState = GameState.SCORES;
308 |
309 | wager = "";
310 | if(timerState) {
311 | System.out.println("Timer cancelled, question closed");
312 | timer.cancel();
313 | timerState = false;
314 | timer = new Timer();
315 | }
316 | if(musicEnabled) {
317 | for (AudioPlayer t : tracks) {
318 | t.pause();
319 | t.rewind();
320 | }
321 | }
322 | break;
323 | case 10: //ENTER
324 | if(Question.getSelected().hasMedia()) {
325 | if(Question.getSelected().getMedia().getType() == Media.MediaType.AUDIO) {
326 | ((AudioPlayer)Question.getSelected().getMedia().getMedia()).pause();
327 | }
328 | }
329 |
330 | if(Player.getActive() != null) {
331 | Player.getActive().changeScore(Question.getSelected().getValue());
332 | System.out.println(Player.getActive().getName() + ": " + Player.getActive().getScore());
333 | }
334 |
335 | if(Round.getCurrentRound().getRoundType() != Round.RoundType.FINAL) {
336 | Question.setSelected(null);
337 | gameState = GameState.ROUND;
338 | }
339 | wager = "";
340 |
341 | if(timerState) {
342 | timer.cancel();
343 | timerState = false;
344 | timer = new Timer();
345 | }
346 |
347 | if(musicEnabled) {
348 | for(AudioPlayer t : tracks) {
349 | t.pause();
350 | t.rewind();
351 | }
352 | }
353 | break;
354 | case 17: //Control
355 | timerState = !timerState;
356 | if(musicEnabled) {
357 | if(tracks[0].position() > 0) {
358 | tracks[0].pause();
359 | tracks[0].rewind();
360 | }
361 | }
362 | if(timerState) {
363 | System.out.println("Timer started");
364 | timer.schedule(new TimerTask() {
365 | @Override public void run() {
366 | System.out.println("Timer called");
367 | if(musicEnabled) tracks[0].play();
368 | timerState = false;
369 | timer = new Timer();
370 | }
371 | }, 5000);
372 | } else {
373 | System.out.println("Timer stopped");
374 | timer.cancel();
375 | timerState = false;
376 | timer = new Timer();
377 | }
378 | break;
379 | case 18: //Option
380 | if(Question.getSelected().hasMedia()) {
381 | Question.getSelected().setShowMedia(!Question.getSelected().isShowMedia());
382 | }
383 | break;
384 | case 61:
385 | if(Question.getSelected().isWagerable()) {
386 | if(wager.length() == 0) {
387 | Question.getSelected().setValue(0);
388 | } else {
389 | try {
390 | Question.getSelected().setValue(Integer.valueOf(wager));
391 | Question.getSelected().setShowQuestion(true);
392 | } catch(NumberFormatException e) {
393 | System.out.println("Failed to set value of wager due to string error");
394 | }
395 | }
396 | }
397 | break;
398 | case 47: /* / */
399 | if(Question.getSelected().isWagerable()) {
400 | Question.getSelected().setShowQuestion(true);
401 | }
402 | break;
403 | case 192:
404 | if(musicEnabled) {
405 | if (Round.getCurrentRound().getRoundType() != Round.RoundType.FINAL) {
406 | if (tracks[0].position() > 0) {
407 | tracks[0].pause();
408 | tracks[0].rewind();
409 | }
410 | tracks[0].play();
411 | } else {
412 | tracks[2].play();
413 | }
414 | }
415 | break;
416 | }
417 | break;
418 |
419 | }
420 |
421 | switch(event.getKeyCode()) { //Always On
422 | case 45:
423 | if(wager.length() > 0) {
424 | wager = wager.substring(0, wager.length() - 1);
425 | System.out.println(wager);
426 | }
427 | break;
428 | case 61: //=
429 | if(Question.getSelected() == null) {
430 | try {
431 | Player.getActive().changeScore(Integer.valueOf(wager));
432 | } catch(NumberFormatException e) {
433 | System.out.println("Wager add failed!");
434 | }
435 | }
436 | wager = "";
437 | break;
438 | case 48:
439 | case 49:
440 | case 50:
441 | case 51:
442 | case 52:
443 | case 53:
444 | case 54:
445 | case 55:
446 | case 56:
447 | case 57:
448 | wager += (event.getKey());
449 | System.out.println(wager);
450 | break;
451 | case 157: //Option
452 | break;
453 | case 44: //<
454 | break;
455 | case 46: //]
456 | break;
457 | case 47: //\
458 | break;
459 | case 37: //Left arrow key (mac)
460 | int leftShift = players.indexOf(Player.getActive())-1;
461 | if(leftShift > -1) {
462 | Player.setActive(players.get(leftShift));
463 | }
464 | break;
465 | case 39: //Right arrow key (mac)
466 | int rightShift = players.indexOf(Player.getActive())+1;
467 | if(rightShift < players.size()) {
468 | Player.setActive(players.get(rightShift));
469 | }
470 | break;
471 | default:
472 | break;
473 | }
474 | }
475 | @Override public void mouseWheel(MouseEvent event) {
476 | if(gameState == GameState.PLAYER_SETUP) boundedScroll(event.getCount());
477 | }
478 |
479 | public void boundedScroll(float change) {
480 | float maxHeight;
481 | float minHeight;
482 | float viewMaxHeight;
483 |
484 | switch(gameState) {
485 | case PLAYER_SETUP:
486 | viewMaxHeight = height;
487 | maxHeight = (viewMaxHeight / 8)*(playerSet.size()*0.75f);
488 | maxOffset = maxHeight;
489 |
490 | minHeight = 0;
491 |
492 | if((vOffset + change) <= minHeight && (vOffset + change) >= maxHeight*-1 && maxHeight > viewMaxHeight) {
493 | vOffset += change;
494 | }
495 | break;
496 | default:
497 | break;
498 | }
499 | }
500 |
501 | private static void progressRound() {
502 | if(progressionPath.peek() != null) {
503 | Round.setCurrentRound(progressionPath.poll());
504 | }
505 | if(Round.getCurrentRound().getRoundType() == Round.RoundType.FINAL) {
506 | Question finalQ = Round.getCurrentRound().getCategories().get(0).getQuestions().get(0);
507 | finalQ.setAnswered(true);
508 | Question.setSelected(finalQ);
509 | gameState = GameState.QUESTION;
510 | }
511 | }
512 |
513 |
514 | private static boolean containsDialogue(String s) {
515 | return s.contains("(") || s.contains(")");
516 | }
517 | private static String removeAlexDialogue(String s) {
518 | return s.substring(0, s.indexOf("(")) + s.substring(s.lastIndexOf(")")+((s.lastIndexOf(")")!=s.length()-1)?(1):(0)), s.length() - 1);
519 | }
520 |
521 | private static void loadCategories(Round r, String filePath, int categoryCount, int categoryQuestionCount) {
522 | JSONParser jsonParser = new JSONParser();
523 |
524 | try {
525 | BufferedReader f = new BufferedReader(new FileReader(new File(filePath)));
526 |
527 | JSONArray categories = (JSONArray) jsonParser.parse(f);
528 | ArrayList choices = new ArrayList<>();
529 |
530 | while (r.getCategories().size() < categoryCount) {
531 | int rand = ThreadLocalRandom.current().nextInt(0, categories.size());
532 | if (!choices.contains(rand)) {
533 | choices.add(rand);
534 | JSONObject cat = (JSONObject) categories.get(rand);
535 | Iterator keys = cat.keySet().iterator();
536 | Iterator values = cat.values().iterator();
537 |
538 | Category c = new Category();
539 |
540 | boolean continueAdd = true;
541 |
542 | while(keys.hasNext() && values.hasNext() && continueAdd) {
543 | switch ((String) keys.next()) {
544 | case "Category":
545 | String catName = (String)values.next();
546 | if(containsDialogue(catName)) {
547 | c.setName(removeAlexDialogue(catName));
548 | c.setDialogue(catName);
549 | } else {
550 | c.setName(catName);
551 | }
552 | break;
553 | case "Clues":
554 | for (Object obj : (JSONArray) values.next()) {
555 | JSONObject clue = (JSONObject) obj;
556 |
557 | Iterator k = clue.keySet().iterator();
558 | Iterator v = clue.values().iterator();
559 | Question q = new Question();
560 |
561 | while (k.hasNext() && v.hasNext()) {
562 | switch ((String) k.next()) {
563 | case "Answer":
564 | String aAdd = ((String) v.next()).trim();
565 | if (!aAdd.equals("") && !aAdd.equals("\u00a0") && !aAdd.equals("=")) { //Null check, shouldn't take place to begin with as I don't believe any null cats exist
566 | q.setAnswer(aAdd);
567 | } else {
568 | c.setQuestions(null);
569 | continueAdd = false;
570 | }
571 | break;
572 | case "Question":
573 | String qAdd = ((String) v.next()).trim();
574 | if (!qAdd.equals("") && !qAdd.equals("\u00a0") && !qAdd.equals("=")) { //Null check, shouldn't take place to begin with as don't think any null cats exist
575 | q.setQuestion(qAdd);
576 | } else {
577 | c.setQuestions(null);
578 | continueAdd = false;
579 | }
580 | break;
581 | case "Media":
582 | Media m = new Media();
583 | JSONObject mediaObj = (JSONObject)v.next();
584 | Iterator mK = mediaObj.keySet().iterator();
585 | Iterator mV = mediaObj.values().iterator();
586 | while(mK.hasNext() && mV.hasNext()) {
587 | switch((String) mK.next()) {
588 | case "Type":
589 | m.setType(Media.MediaType.valueOf(((String)mV.next()).toUpperCase()));
590 | break;
591 | case "Name":
592 | m.setName((String)mV.next());
593 | break;
594 | case "Path":
595 | m.setPath((String)mV.next());
596 | break;
597 | }
598 | }
599 | q.setMedia(m);
600 | break;
601 | default:
602 | break;
603 | }
604 | }
605 | if(continueAdd) {
606 | if(q.getQuestion() != null && q.getAnswer() != null) {
607 | c.addQuestion(q);
608 | } else {
609 | continueAdd = false;
610 | c.setQuestions(null);
611 | break;
612 | }
613 | } else {
614 | break;
615 | }
616 | }
617 | break;
618 | case "Date":
619 | c.setDate((String)values.next());
620 | break;
621 | }
622 | }
623 |
624 | if(c.getQuestions() != null) {
625 | if(!useFilter || isCustom || (c.getYear() >= filterYear && c.getYear() <= upperFilterYear && c.getQuestions().size() == categoryQuestionCount)) {
626 | r.addCategory(c);
627 | for (Question cq : c.getQuestions()) {
628 | if (c.hasDialogue()) {
629 | cq.setDialogue(c.getDialogue());
630 | }
631 | }
632 | }
633 | }
634 | }
635 | }
636 | } catch(ParseException | IOException e){
637 | e.printStackTrace();
638 | }
639 | }
640 | private static void loadCategories(Round r, int season) {
641 | useFilter = false;
642 | loadCategories(r, "data" + File.separator + "questions" + File.separator + "by_season" + File.separator + r.getRoundType().toString().toLowerCase() +"_jeopardy_season_"+season+".json", (r.getRoundType() == Round.RoundType.FINAL) ? (1) : (6), (r.getRoundType() == Round.RoundType.FINAL) ? (1) : (5));
643 | useFilter = true;
644 | }
645 | private static void loadCategories(Round r, Object[][] categories) { //Bad form to use 2d object array rather than 3 unique arrays (or an object) but EH
646 | Round[] choiceRounds = new Round[categories.length];
647 | for(int i = 0; i < categories.length; i++) {
648 | choiceRounds[i] = new Round(r.getRoundType());
649 | if (categories[i].length != 3){
650 | System.out.println("Mismatched argument lengths! Loading round from random season...");
651 | loadCategories(r, (int) (Math.random() * 35) + 1);
652 | return;
653 | } else {
654 | try {
655 | loadCategories(choiceRounds[i], (String)categories[i][0], (int)categories[i][1], (int)categories[i][2]);
656 | } catch(ClassCastException e) {
657 | System.out.println("Incorrect argument types! Loading round from a random season...");
658 | loadCategories(r, (int) (Math.random() * 35) + 1);
659 | return;
660 | }
661 | for(Category c : choiceRounds[i].getCategories()) {
662 | r.addCategory(c);
663 | }
664 | }
665 | }
666 | }
667 |
668 | private static ArrayList loadPlayerData(String filePath) {
669 | try(BufferedReader f = new BufferedReader(new FileReader(filePath))) {
670 | ArrayList ps = new ArrayList();
671 | String line;
672 |
673 | while((line = f.readLine()) != null) {
674 | if(!line.isEmpty()) {
675 | String[] parts = line.split("\\|");
676 | ps.add(new Player(parts[0].trim(), Integer.parseInt(parts[1].trim())));
677 | }
678 | }
679 | return ps;
680 | } catch(IOException e) {
681 | System.out.println("Could not locate player file!");
682 | return null;
683 | }
684 | }
685 |
686 | public static void printQuestions(Round r) {
687 | System.out.println(r.getRoundType().toString());
688 | for(Category c : r.getCategories()) {
689 | System.out.println(c.getName());
690 | int count = 0;
691 | for(Question q : c.getQuestions()) {
692 | System.out.println("["+count++ +"]");
693 | System.out.println(q.getQuestion());
694 | System.out.println(q.getAnswer());
695 | }
696 | }
697 | }
698 |
699 | public static void setProgressionPath(Round... roundPath) {
700 | Collections.addAll(progressionPath, roundPath);
701 | }
702 |
703 | public static void scrapeGame(String url) {
704 | try {
705 | Process p = Runtime.getRuntime().exec("scripts" + File.separator + "scraper.py -s " + url);
706 | p.waitFor();
707 | } catch(IOException | InterruptedException e) {
708 | System.out.println("Scraper call failed!");
709 | }
710 | }
711 | public static void makeCustoms() {
712 | try {
713 | Process p = Runtime.getRuntime().exec("scripts" + File.separator + "customs.py");
714 | p.waitFor();
715 | } catch(IOException | InterruptedException e) {
716 | System.out.println("Failed to make customs!");
717 | }
718 | }
719 |
720 | public static void main(String[] args) {
721 | // makeCustoms();
722 | if(!isCustom) {
723 | setProgressionPath(first, second, third); //Shouldn't have named round object, should migrate this to have loadCategories return Round objects to pass in
724 | for(Round r : progressionPath) {
725 | r.setFilterYear(filterYear);
726 | }
727 |
728 | String append = "";
729 | String dir = "all";
730 |
731 | if(isScraped) {
732 | scrapeGame("http://www.j-archive.com/showgame.php?game_id=6682");
733 | append = "_scraped";
734 | dir = "scrape";
735 | useFilter = false;
736 |
737 | }
738 |
739 | // loadCategories(first, new Object[][]{
740 | // {"data" + File.separator + "questions" + File.separator + "by_season" + File.separator + "single_jeopardy_season_35.json", 1, 5},
741 | // {"data" + File.separator + "questions" + File.separator + "by_season" + File.separator + "single_jeopardy_season_34.json", 2, 5},
742 | // {"data" + File.separaftor + "questions" + File.separator + "by_season" + File.separator + "single_jeopardy_season_33.json", 3, 5},
743 | // }); //Template for loading from multiple files, should improve this but oh well
744 |
745 | loadCategories(first, "data" + File.separator + "questions" + File.separator + dir + File.separator + "single_jeopardy" + append + ".json", 6, 5);
746 | loadCategories(second,"data" + File.separator + "questions" + File.separator + dir + File.separator + "double_jeopardy" + append + ".json", 6, 5);
747 | loadCategories(third, "data" + File.separator + "questions" + File.separator + dir + File.separator + "final_jeopardy" + append + ".json", 1, 1);
748 |
749 | for(Round r : progressionPath) {
750 | printQuestions(r);
751 | r.setWagerables();
752 | }
753 | } else {
754 | setProgressionPath(first);
755 | loadCategories(first, "/Users/zackamiton/Code/Jeopardizer/data/questions/custom/custom.json", 1, 1);
756 | }
757 |
758 | for (String p : playerNames) {
759 | players.add(new Player(p));
760 | }
761 |
762 | if(players.size() > 0) {
763 | Player.setActive(players.get(0));
764 | }
765 |
766 | app.args = new String[]{"Game"};
767 | console.args = new String[]{"Console"};
768 |
769 | // playerSet = loadPlayerData("data" + File.separator + "players" + File.separator + "data.txt");
770 |
771 | PApplet.runSketch(app.args, app);
772 | PApplet.runSketch(console.args, new Console());
773 | }
774 | }
775 |
--------------------------------------------------------------------------------