Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
587 lines (558 sloc) 17.9 KB
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>High School Game</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.2/html5shiv-printshiv.min.js"></script>
<![endif]-->
<style>
/* CSS reset */
article,aside,figure,footer,header,hgroup,menu,nav,section { display:block; }
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,
legend,input,button,textarea,select,p,blockquote,th,td{margin:0;padding:0}
h1,h2,h3,h4,h5,h6 {font-size: 100%;font-weight: inherit;}
img {color: transparent; font-size: 0; border: 0; vertical-align: middle;
-ms-interpolation-mode: bicubic;}
html {
font-size: 2em;
background-color: #ffeed4;
}
#player_viewport {
max-width: 35em;
margin: 1em auto;
}
.option-wrapper {
margin: 1em 1em;
}
a.option {
display: inline-block;
text-decoration: none;
font-weight: bold;
color: #68f;
}
a.option:hover, a.option:active, a.option:focus {
color: #d4f;
text-shadow: 0 0 0.06em #94f;
}
a.option.already-tried {
text-decoration: line-through;
color: #666;
}
.description-of-dilemma {
margin: 1em 0;
}
.status-area-wrapper {
float: right;
position: relative;
}
.status-area {
margin-left: 1em;
padding: 1em;
border: 2px solid green;
font-size: 0.8em;
width: 19.3em;
background-color: #fff;
position: relative;
}
.old-status-area {
position: absolute;
top: 0;
left: 0;
}
.status-character-name {
font-weight: bold;
text-align: center;
}
.status-bars {
margin-bottom: 0.8em;
}
.status-bar {
font-family: monospace;
}
.trait-list {
margin-top: 1em;
}
.example-symbol {
opacity: 0.3;
}
p.chosen {
font-weight: bold;
margin-top: 1em;
margin-bottom: 0.1em;
}
@media (max-width: 60em) {
.trait-list {
display: none;
}
.status-area-wrapper {
float: none;
display: block;
}
.status-area {
margin: 0;
border-width: 0 0 2px;
width: 100%;
}
#player_viewport {
margin: 0;
}
.main {
margin: 0.5em 1em;
}
}
</style>
</head>
<body>
<div id="player_viewport">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<!-- name source: US Census: -->
<script src="among_female_usians_pretty_uncommon_names.js"></script>
<script src="among_male_usians_pretty_uncommon_names.js"></script>
<script>
(function () {
"use strict";
function assert(x) {
if(!x) {
throw 'error!';
}
}
window.Eli = {};
Eli.traits = {
daydream: {
description: "You can daydream whenever you want.",
options: {
name: "daydream",
tags: ['daydream'],
do: function(dilemma) {
dilemma.actor.messages.push("You daydream about popsicles.");
return 'popsicle';
}
}
},
video_gaming: function() { return {
description: "When you're at home, you can play video games.",
options: function(dilemma) {
if(dilemma.actor.location === 'home') { return {
name: "play video games",
tags: ['video_games'],
do: function(dilemma) {
var score = (Math.round(Math.pow(1.03, _.random(2, 1000))) + 2);
dilemma.actor.messages.push("You score " + score + " points!");
return ['score', (score > 1000000 ? 'high score' : null)];
}
}; }
}
};},
video_gaming_affects_your_status: function(status_id_to_buff, amount) { return {
description: ("When you play video games, you feel more " + status_id_to_buff
+ ". " + Eli.status_buff_symbols(status_id_to_buff, amount)),
effect: function(character, tags) {
if(_.contains(tags, 'video_games')) {
increment_status(character, status_id_to_buff, amount);
character.messages.push("You feel more " + status_id_to_buff + "! " +
Eli.status_buff_symbols(status_id_to_buff, amount));
}
}
};},
go_to_school: {
description: "You can go to school [during school hours?].",
options: function(dilemma) {
if(dilemma.actor.location !== 'school') { return {
name: "go to school",
tags: ['go', 'go_to_school'],
do: function(dilemma) {
dilemma.actor.location = 'school';
dilemma.actor.messages.push("You take the bus to school.");
}
}; }
}
},
i_hate_school: {
description: "You don't always go to school because you don't like it.",
pattern: function(dilemma, option) {
if(_.contains(option.tags, 'go_to_school')) {
if(_.random(0,1) === 0) {
dilemma.actor.messages.push("You decide against going to school.");
return 'fail';
}
}
}
},
too_calm_to_swear: {
description: "You only swear when you're excitable.",
pattern: function(dilemma, option) {
if(_.contains(option.tags, 'swear')) {
if(get_status(dilemma.actor, 'calm') + _.random(-1, 1) > 1) {
dilemma.actor.messages.push("You are too calm to feel like swearing.");
return 'fail';
}
}
}
},
go_to_home: {
description: "You can go home.",
options: function(dilemma) {
if(dilemma.actor.location !== 'home') { return {
name: "go home",
tags: ['go', 'go_home'],
do: function(dilemma) {
dilemma.actor.location = 'home';
dilemma.actor.messages.push("You walk home.");
}
}; }
}
},
swear: {
description: "You can swear whenever you want.",
options: {
instant: true,
name: "swear",
tags: ['swear'],
do: function(dilemma) {
dilemma.actor.messages.push("Fuck!");
}
}
},
dud: null //just so we can put a comma after all real ones consistently
};
function look_up_trait(name_and_args) {
var name;
var args;
if (_.isString(name_and_args)) {
name = name_and_args;
args = [];
} else {
name = name_and_args[0];
args = _.rest(name_and_args);
}
var t = Eli.traits[name];
if (_.isFunction(t)) {
t = t.apply(null, args);
}
return t;
}
// only the options that are applicable right now
function get_trait_options(dilemma, trait) {
if (!trait.options) {
return [];
}
var c = trait.options;
if (_.isFunction(c)) {
c = c(dilemma);
}
if (!c) {
c = [];
}
if (!_.isArray(c)) {
c = [c];
}
return c;
}
function get_character_options(dilemma) {
return _.flatten(_.map(dilemma.actor.traits, function(t) {
return get_trait_options(dilemma, look_up_trait(t));
}), true);
}
function get_remaining_character_options(dilemma) {
return _.filter(get_character_options(dilemma), function(option) {
return !dilemma.already_tried[option.name];
});
}
Eli.game_state = {
todo: [], // a stack that you push/pop on the end
phase: 0,
characters: {},
characters_list: [] // order doesn't matter?
};
var game_state = Eli.game_state;
var Bob = {
name: "Bob",
traits: [
'daydream',
'video_gaming',
['video_gaming_affects_your_status', 'confident'],
'swear',
'go_to_school',
'go_to_home',
'i_hate_school',
'too_calm_to_swear'
],
location: 'school',
old_messages: [],
messages: [],
status: {
confidence: 2,
excitement: -1
},
relationships: []
};
function make_character (character) {
game_state.characters_list.push (character);
game_state.characters [character.name] = character;
}
function random_unused_name() {
var name_list = _.sample([Eli_male_names, Eli_female_names]);
for(var i = 1; i < 1000; ++i) {
var prospective_name = _.sample(name_list);
if (game_state.characters[prospective_name] === undefined) {
return prospective_name;
}
}
throw "Couldn't find unique name"
}
make_character (Bob);
for (var whatever = 0; whatever <100;++ whatever) {
var character = {name: random_unused_name(), traits: [], location: "home", old_messages: [], messages: [], status: {}, relationships: []};
character.traits.push ("daydream");
character.traits.push ("go_to_school");
character.traits.push ("go_to_home");
make_character (character);
}
for (var whatever = 0; whatever <100;++ whatever) {
var bottle_1 =_.random (0, game_state.characters_list.length-1);
var bottle_2 =_.random (0, game_state.characters_list.length-1);
if (bottle_1!== bottle_2) {
var character_1 =game_state.characters_list [bottle_1];
var character_2 = game_state.characters_list [bottle_2];
var relationship = "friend";
character_1.relationships.push ([character_2.name, relationship]);
character_2.relationships.push ([character_1.name, relationship]);
}
}
// Make sure save games can't run scripts or stuff.
// The result of this function will be passed to
// innerHTML= or more specifically,
// $(...).html(Eli.check_message_html(....))
Eli.check_message_html = function(message) {
if(!/^([^<>&]*(&lt;|&gt;|&amp;|&quot;|<p class="chosen">|<\/p>|<span( style="((color|background-color): [a-zA-Z0-9#%() ,]*(;|(?= *")) *)*")?>|<\/span>))*[^<>&]*$/.test(message)) {
throw "Uncheckable message: " + message;
}
return message;
}
Eli.symbol = function(text_content, text_color, background_color) {
return $('<span/>').css({
"color": text_color,
"background-color": background_color
}).text(text_content).prop('outerHTML');
};
Eli.status_buff_symbols = function(status_id_to_buff, amount) {
if(amount === undefined) { amount = 1; }
return Eli.status_types[status_id_to_buff].symbol.repeat(amount);
}
Eli.status_max = 3;
Eli.neutral_status_symbol = Eli.symbol('0', '#bbb', '#555');
Eli.general_status_types_list = [{
id: 'confidence',
positive: {
name: "confident",
symbol: Eli.symbol('+', '#fff', '#00f')
},
negative: {
name: "crushed",
symbol: Eli.symbol('-', '#00f', '#000')
}
},
{
id: 'excitement',
positive: {
name: "excited",
symbol: Eli.symbol('!', '#ff0', '#f00')
},
negative: {
name: "calm",
symbol: Eli.symbol('~', '#00f', '#ccf')
}
}
];
Eli.general_status_types = {};
Eli.status_types = {};
Eli.max_status_name_length = 0;
for (var i = 0; i < Eli.general_status_types_list.length; ++i) {
var status = Eli.general_status_types_list[i];
Eli.general_status_types[status.id] = status;
Eli.status_types[status.positive.name] = status.positive;
Eli.status_types[status.negative.name] = status.negative;
status.positive.opposite = status.negative;
status.negative.opposite = status.positive;
status.positive.general = status;
status.negative.general = status;
status.positive.sign = 1;
status.negative.sign = -1;
Eli.max_status_name_length = Math.max(Eli.max_status_name_length,
status.positive.name.length, status.negative.name.length);
}
function get_status(character, status_name) {
var status_type = Eli.status_types[status_name];
return character.status[status_type.general.id] * status_type.sign;
}
function set_status(character, status_name, value) {
assert(Math.round(value) === value && Math.abs(value) <= Eli.status_max);
var status_type = Eli.status_types[status_name];
character.status[status_type.general.id] = value * status_type.sign;
assert(get_status(character, status_name) === value);
}
function increment_status(character, status_name, amount) {
if(amount === undefined) { amount = 1; }
assert(Math.round(amount) === amount && amount > 0);
var old_value = get_status(character, status_name);
var new_value = Math.min(old_value + amount, Eli.status_max);
set_status(character, status_name, new_value);
return new_value - old_value;
}
// assumes monospace
Eli.pad_left = function(html_text, count, pad_character) {
var text_length = $('<div/>').html(html_text).text().length;
if (pad_character === undefined) { pad_character = '&nbsp;'; }
return (pad_character.repeat(count - text_length) + html_text);
};
Eli.pad_right = function(html_text, count, pad_character) {
var text_length = $('<div/>').html(html_text).text().length;
if (pad_character === undefined) { pad_character = '&nbsp;'; }
return (html_text + pad_character.repeat(count - text_length));
};
function example_symbol(symbol) {
return $('<span/>').addClass('example-symbol').html(symbol).prop('outerHTML');
}
Eli.status_bar = function(character, general_status_type) {
var negative_points = Math.max(0, -character.status[general_status_type.id]);
var positive_points = Math.max(0, character.status[general_status_type.id]);
return $('<span/>').addClass('status-bar').html(
Eli.pad_left(general_status_type.negative.name, Eli.max_status_name_length+2) + ' ' + example_symbol(general_status_type.negative.symbol) +
' [' + Eli.pad_left(general_status_type.negative.symbol.repeat(negative_points), Eli.status_max) +
Eli.neutral_status_symbol +
Eli.pad_right(general_status_type.positive.symbol.repeat(positive_points), Eli.status_max) + '] ' +
example_symbol(general_status_type.positive.symbol) + ' ' + Eli.pad_right(general_status_type.positive.name, Eli.max_status_name_length+2)
);
};
Eli.status_area = function(character) {
var $who = $('<div/>').addClass('status-character-name').text(character.name);
var $status_bars = $('<div/>').addClass('status-bars');
for (var i = 0; i < Eli.general_status_types_list.length; ++i) {
var general_status_type = Eli.general_status_types_list[i];
if (_.has(character.status, general_status_type.id)) {
$status_bars.append($('<div/>').append(Eli.status_bar(character, general_status_type)));
}
}
var $traits = $('<div/>').addClass('trait-list');
_.each(_.map(character.traits, look_up_trait), function(trait) {
$traits.append($('<p/>').html(Eli.check_message_html(trait.description)));
});
var $relationships = $("<div/>").addClass ("relationship_list");
_.each (character.relationships, function (relationship) {
$relationships.append ($("<p/>").text (relationship [0] + " is your "+ relationship [1]));
});
return $('<aside/>').addClass('status-area').append(
$who,
$status_bars,
$('<p/>').text("You are at " + character.location + '.'),
$relationships,
$traits
);
};
function possibly_go_to_the_next_phase() {
if(game_state.todo.length === 0) {
game_state.phase += 1;
_.each(_.shuffle(game_state.characters_list), function(c) {
game_state.todo.push({ actor: c, already_tried: {}});
});
}
}
function current_dilemma() {
return _.last(game_state.todo);
}
function dilemma_requires_player_input(dilemma) {
return _.random(0,1) === 0;
}
function make_tags_happen_to_character(character, tags) {
_.each(_.map(character.traits, look_up_trait), function(trait) {
if(trait.effect) {
trait.effect(character, tags);
}
});
}
// returns true if succeeded
function try_to_make_choice(dilemma, option) {
var character = dilemma.actor;
dilemma.already_tried[option.name] = true;
//var succeeded = _.random(0,10) > 3;
var succeeded = true;
//var message_index = character.messages.length;
//character.messages.push($('<p/>').addClass('chosen').text('You try to '+option.name+', but...').prop('outerHTML'));
//character.messages.push($('<p/>').addClass('chosen').text('You try to '+option.name+'.').prop('outerHTML'));
// You reluctantly ___»«
character.messages.push($('<p/>').addClass('chosen').text('» '+option.name+' «').prop('outerHTML'));
_.find(_.map(character.traits, look_up_trait), function(trait) {
if(trait.pattern) {
if(trait.pattern(dilemma, option) === 'fail') {
succeeded = false;
return true; // break
}
}
});
if (succeeded) {
if(!option.instant) {
var d = game_state.todo.pop(); assert(d == dilemma);
}
//character.messages[message_index] = $('<p/>').addClass('chosen').text('You '+option.name+'.').prop('outerHTML');
var result_tags = option.do(dilemma);
var tags = _.uniq(_.filter(_.flatten([option.tags, result_tags]), _.isString));
make_tags_happen_to_character(character, tags);
} else {
}
return succeeded;
}
function make_choice_automatically(dilemma) {
try_to_make_choice(dilemma, _.sample(get_remaining_character_options(dilemma)));
}
function get_to_the_next_point_where_a_player_character_has_a_choice() {
while(true) {
possibly_go_to_the_next_phase();
assert(game_state.todo.length > 0);
if(dilemma_requires_player_input(current_dilemma())) {
break;
} else {
make_choice_automatically(current_dilemma());
}
}
var $vp = $('#player_viewport');
var $old_status_area = $('#player_viewport .status-area:not(.old-status-area)').detach();
$vp.empty();
var dilemma = current_dilemma();
$old_status_area.addClass('old-status-area');
var $new_status_area = Eli.status_area(dilemma.actor);
var $status_area_wrapper = $('<div/>').addClass('status-area-wrapper').append($old_status_area, $new_status_area);
$new_status_area.css({opacity: 0}).animate({opacity: 1}, function() { $old_status_area.remove(); });
$vp.append($status_area_wrapper);
var $main = $('<div/>').addClass('main').css({opacity: 0}).animate({opacity: 1});
_.each(dilemma.actor.messages, function(message) {
$main.append($('<p/>').html(Eli.check_message_html(message)));
});
$main.append($('<p/>').addClass('description-of-dilemma').text("You have a choice!!!!!"));
_.each(get_character_options(dilemma), function(option) {
var available = !dilemma.already_tried[option.name];
if(available) {
var $option = $('<a/>').attr('href', '#javascript').addClass('option').text(option.name).on('click', function(e) {
e.stopPropagation();
e.preventDefault();
// should this happen always? or just on success
Array.prototype.push.apply(dilemma.actor.old_messages, dilemma.actor.messages);
dilemma.actor.messages = [];
var succeeded = try_to_make_choice(dilemma, option);
get_to_the_next_point_where_a_player_character_has_a_choice();
});
} else {
var $option = $('<a/>').addClass('option').text(option.name).addClass('already-tried');
}
$main.append($('<div/>').addClass('option-wrapper').append($option));
});
$vp.append($main);
}
get_to_the_next_point_where_a_player_character_has_a_choice();
})();
</script>
</body>
</html>