├── 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 |
12 | 41 |
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 |
15 |
16 | Game Setup 17 |

18 | 19 | 20 | * 21 |

22 |

23 | 24 |
25 |

26 |
27 |
28 |

29 | 30 | 31 |   32 | * 33 |

34 |

35 | 36 | 37 |

38 |

39 | 40 | 41 |

42 |

43 | 44 | 45 |

46 |

47 | 48 | 49 | 50 |

51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 |
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 | ![Question View](./media/setup.png) 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 | --------------------------------------------------------------------------------