Select any categories below that you'd like to play.
You can choose as many as you'd like.
{props.prompts.map(
prompt =>
)}
);
});
const LetsPlay = React.memo(props => {
const quorum = Math.max(0, props.num_players - props.prompts.length);
const max_votes = props.prompts.reduce(
(max_so_far, v) => Math.max(max_so_far, v.votes.length), 0);
if (max_votes < quorum) {
let text = `Before we play, we should collect a bit
more information about what category would
be interesting for this group. So, either
type a new category option above, or else`;
if (props.prompts.length) {
if (props.prompts.length > 1)
text += " vote on some of the categories below.";
else
text += " vote on the category below.";
} else {
text += " wait for others to submit, and then vote on them below.";
}
return (
Click/tap on each pair of answers that should be scored as equivalent,
(or click a word twice to split it out from a group). Remember,
what goes around comes around, so it's best to be generous when
judging.
Also, for an especially fun or witty answer, you can give kudos
by clicking the star on the right. You may only do this for one
word/group.
);
}
}
class ActivePrompt extends React.PureComponent {
constructor(props) {
super(props);
const items = props.prompt.items;
this.submitted = false;
this.answers = [...Array(items)].map(() => React.createRef());
this.answering_sent_recently = false;
this.handle_submit = this.handle_submit.bind(this);
this.handle_change = this.handle_change.bind(this);
}
handle_change(event) {
/* We don't care (or even look) at what the player is typing at
* this point. We simply want to be informed that the player _is_
* typing so that we can tell the server (which will tell other
* players) that there is activity here.
*/
/* Rate limit so that we don't send an "answering" notification
* more frequently than necessary.
*/
if (! this.answering_sent_recently) {
fetch_post_json(`answering/${this.props.prompt.id}`);
this.answering_sent_recently = true;
setTimeout(() => { this.answering_sent_recently = false; }, 1000);
}
}
async handle_submit(event) {
const form = event.currentTarget;
/* Prevent the default page-changing form-submission behavior. */
event.preventDefault();
/* And don't submit a second time. */
if (this.submitted)
return;
const response = await fetch_post_json(`answer/${this.props.prompt.id}`, {
answers: this.answers.map(r => r.current.value)
});
if (response.status === 200) {
const result = await response.json();
if (! result.valid) {
add_message("danger", result.message);
return;
}
} else {
add_message("danger", "An error occurred submitting your answers");
return;
}
/* Everything worked. Server is happy with our answers. */
form.reset();
this.submitted = true;
}
render() {
let still_waiting = null;
const answering_players = Object.keys(this.props.players_answering);;
if (answering_players.length) {
still_waiting = (
Still waiting for the following player
{answering_players.length > 1 ? 's' : ''}
:
{answering_players.map(player => {
return (
{player}{' '}
{'.'}{'.'}{'.'}
);
})}
);
}
let move_on_button = null;
if (this.props.idle) {
move_on_button =(
);
}
if (this.props.players_answered.has(this.props.player.name)) {
return (
Submission received
The following players have submitted their answers:{' '}
{[...this.props.players_answered].join(', ')}
{still_waiting}
{move_on_button}
);
}
return (
The Game of Empathy
Remember, you're trying to match your answers with
what the other players submit.
Give {this.props.prompt.items} answer
{this.props.prompt.items > 1 ? 's' : ''} for the following prompt:
{this.props.prompt.prompt}
);
}
}
class Game extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
game_info: {},
player_info: {},
other_players: [],
prompts: [],
active_prompt: null,
players_answered: new Set(),
players_answering: {},
answering_idle: false,
end_answers_votes: new Set(),
ambiguities: null,
players_judged: new Set(),
players_judging: {},
judging_idle: false,
end_judging_votes: new Set(),
scores: null,
new_game_votes: new Set(),
ready: false
};
}
set_game_info(info) {
this.setState({
game_info: info
});
}
set_player_info(info) {
this.setState({
player_info: info
});
}
set_other_player_info(info) {
const other_players_copy = [...this.state.other_players];
const idx = other_players_copy.findIndex(o => o.id === info.id);
if (idx >= 0) {
other_players_copy[idx] = info;
} else {
other_players_copy.push(info);
}
this.setState({
other_players: other_players_copy
});
}
disable_player(info) {
const idx = this.state.other_players.findIndex(o => o.id === info.id);
if (idx < 0)
return;
const other_players_copy = [...this.state.other_players];
other_players_copy[idx].active = false;
this.setState({
other_players: other_players_copy
});
}
reset_game_state() {
this.setState({
prompts: [],
active_prompt: null,
players_answered: new Set(),
players_answering: {},
answering_idle: false,
end_answers_votes: new Set(),
ambiguities: null,
players_judged: new Set(),
players_judging: {},
judging_idle: false,
end_judging_votes: new Set(),
scores: null,
new_game_votes: new Set(),
ready: false
});
}
set_prompts(prompts) {
this.setState({
prompts: prompts
});
}
add_or_update_prompt(prompt) {
const prompts_copy = [...this.state.prompts];
const idx = prompts_copy.findIndex(p => p.id === prompt.id);
if (idx >= 0) {
prompts_copy[idx] = prompt;
} else {
prompts_copy.push(prompt);
}
this.setState({
prompts: prompts_copy
});
}
set_active_prompt(prompt) {
this.setState({
active_prompt: prompt
});
}
set_players_answered(players) {
this.setState({
players_answered: new Set(players)
});
}
set_player_answered(player) {
const new_players_answering = {...this.state.players_answering};
delete new_players_answering[player];
this.setState({
players_answered: new Set([...this.state.players_answered, player]),
players_answering: new_players_answering
});
}
set_players_answering(players) {
const players_answering = {};
for (let player of players) {
players_answering[player] = {active: false};
}
this.setState({
players_answering: players_answering
});
}
set_player_answering(player) {
/* Set the player as actively answering now. */
this.setState({
players_answering: {
...this.state.players_answering,
[player]: {active: true}
}
});
/* And arrange to have them marked idle very shortly.
*
* Note: This timeout is intentionally very, very short. We only
* need it long enough that the browser has latched onto the state
* change to "active" above. We actually use a CSS transition
* delay to control the user-perceptible length of time after
* which an active player appears inactive.
*/
setTimeout(() => {
this.setState({
players_answering: {
...this.state.players_answering,
[player]: {active: false}
}
});
}, 100);
}
set_answering_idle(value) {
this.setState({
answering_idle: value
});
}
set_end_answers(players) {
this.setState({
end_answers_votes: new Set(players)
});
}
set_player_vote_end_answers(player) {
this.setState({
end_answers_votes: new Set([...this.state.end_answers_votes, player])
});
}
set_player_unvote_end_answers(player) {
this.setState({
end_answers_votes: new Set([...this.state.end_answers_votes].filter(p => p !== player))
});
}
set_ambiguities(ambiguities) {
this.setState({
ambiguities: ambiguities
});
}
set_players_judged(players) {
this.setState({
players_judged: new Set(players)
});
}
set_player_judged(player) {
const new_players_judging = {...this.state.players_judging};
delete new_players_judging[player];
this.setState({
players_judged: new Set([...this.state.players_judged, player]),
players_judging: new_players_judging
});
}
set_players_judging(players) {
const players_judging = {};
for (let player of players) {
players_judging[player] = {active: false};
}
this.setState({
players_judging: players_judging
});
}
set_player_judging(player) {
/* Set the player as actively judging now. */
this.setState({
players_judging: {
...this.state.players_judging,
[player]: {active: true}
}
});
/* And arrange to have them marked idle very shortly.
*
* Note: This timeout is intentionally very, very short. We only
* need it long enough that the browser has latched onto the state
* change to "active" above. We actually use a CSS transition
* delay to control the user-perceptible length of time after
* which an active player appears inactive.
*/
setTimeout(() => {
this.setState({
players_judging: {
...this.state.players_judging,
[player]: {active: false}
}
});
}, 100);
}
set_judging_idle(value) {
this.setState({
judging_idle: value
});
}
set_end_judging(players) {
this.setState({
end_judging_votes: new Set(players)
});
}
set_player_vote_end_judging(player) {
this.setState({
end_judging_votes: new Set([...this.state.end_judging_votes, player])
});
}
set_player_unvote_end_judging(player) {
this.setState({
end_judging_votes: new Set([...this.state.end_judging_votes].filter(p => p !== player))
});
}
set_scores(scores) {
this.setState({
scores: scores
});
}
set_new_game_votes(players) {
this.setState({
new_game_votes: new Set(players)
});
}
set_player_vote_new_game(player) {
this.setState({
new_game_votes: new Set([...this.state.new_game_votes, player])
});
}
set_player_unvote_new_game(player) {
this.setState({
new_game_votes: new Set([...this.state.new_game_votes].filter(p => p !== player))
});
}
state_ready() {
this.setState({
ready: true
});
}
render() {
const state = this.state;
if (state.scores) {
const players_total = state.players_answered.size;
let perfect_score = 0;
for (let i = 0;
i < state.active_prompt.items &&
i < state.scores.words.length;
i++)
{
perfect_score += state.scores.words[i].players.length;
}
return (
{state.active_prompt.prompt}
Scores
{state.scores.scores.map(score => {
let perfect = null;
if (score.score === perfect_score) {
perfect = Perfect!;
}
let quirkster = null;
if (score.score === state.active_prompt.items) {
quirkster = Quirkster!;
}
let kudos_slam = null;
if (score.kudos > 0 && score.kudos >= players_total - 1) {
kudos_slam = Kudos Slam!;
}
return (