tabs, pages, controls

This commit is contained in:
Nicky Case 2017-06-28 10:53:43 -04:00
parent 14bf47f11a
commit e8c18fdbe9
15 changed files with 452 additions and 214 deletions

BIN
assets/sandbox_tabs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -5,11 +5,17 @@
}
body{
margin:0;
overflow: hidden;
background: #fff; /*url('paper@2x.png');
background-size: 100px 100px;*/
font-family: 'FuturaHandwritten';
color: #333;
font-size: 20px;
}
#main{
width: 100%;
@ -30,7 +36,7 @@ body{
#slideshow{
/*background: #bada55;*/
/*border: 1px solid rgba(0,0,0,0.2);*/
border: 1px solid rgba(0,0,0,0.2);
width:960px;
height:540px;
@ -55,15 +61,15 @@ body{
/******** Text Box ********/
.textbox{
font-family: 'FuturaHandwritten';
color: #333;
font-size: 20px;
}
.textbox{}
.textbox > div{
position: absolute;
}
.label{
position: absolute;
}
/********* Button ********/
.button{
@ -112,6 +118,33 @@ body{
display: none;
}
/*************************/
/****** SANDBOX UI *******/
/*************************/
#sandbox_tabs{
position: absolute;
left: 460px; top:-10px;
background: url(../assets/sandbox_tabs.png);
width:500px; height:470px;
background-size: auto 100%;
}
#sandbox_tabs > div{
position: absolute;
}
#sandbox_tabs .hitbox{
cursor: pointer;
font-size: 25px;
top: 22px;
}
#sandbox_tabs .sandbox_page{
width: 433px;
height: 385px;
left: 33px;
top: 80px;
}
/*************************/
/***** SLIDE SELECT ******/
/*************************/

View file

@ -16,7 +16,7 @@
<!-- Libraries -->
<script src="js/lib/helpers.js"></script>
<script src="js/lib/pegasus.min.js"></script>
<script src="js/lib/pegasus.js"></script>
<script src="js/lib/minpubsub.src.js"></script>
<script src="js/lib/q.js"></script>
<script src="js/lib/pixi.min.js"></script>
@ -36,9 +36,10 @@
<script src="js/sims/PD.js"></script>
<script src="js/sims/SillyPixi.js"></script>
<script src="js/sims/Tournament.js"></script>
<script src="js/sims/SandboxUI.js"></script>
<!-- Slides -->
<script src="js/slides/Slides_Ecology.js"></script>
<script src="js/slides/Slides_Sandbox.js"></script>
<!-- Main Code -->
<script>

View file

@ -24,7 +24,10 @@ function Button(config){
// Customize DOM
button.style.left = config.x+"px";
button.style.top = config.y+"px";
text.innerHTML = Words.get(config.text_id);
config.upperCase = (config.upperCase===undefined) ? true : config.upperCase;
var words = Words.get(config.text_id);
if(config.upperCase) words=words.toUpperCase();
text.innerHTML = words;
// On hover...
hitbox.onmouseover = function(){

View file

@ -46,6 +46,18 @@ var _removeFade = function(self, INSTANT){
}
};
// Make Label
var _makeLabel = function(wordID, x, y, width, height){
var dom = document.createElement("div");
dom.className = "label";
if(x!==undefined) dom.style.left = x+"px";
if(y!==undefined) dom.style.top = y+"px";
if(width!==undefined) dom.style.width = width+"px";
if(height!==undefined) dom.style.height = height+"px";
dom.innerHTML = Words.get(wordID);
return dom;
};
/*******
Make a Sprite. e.g:

53
js/lib/pegasus.js Normal file
View file

@ -0,0 +1,53 @@
// a url (naming it a, because it will be reused to store callbacks)
// e timeout error placeholder to avoid using var, not to be used
// xhr placeholder to avoid using var, not to be used
function pegasus(a, e, xhr) {
xhr = new XMLHttpRequest();
// Set URL
xhr.open('GET', a);
// Don't need a to store URL anymore
// Reuse it to store callbacks
a = [];
pegasus.timeout && (xhr.timeout = pegasus.timeout);
xhr.ontimeout = function (event) {
e = event
}
xhr.onreadystatechange = xhr.then = function(onSuccess, onError, cb, data) {
// Test if onSuccess is a function
// Means that the user called xhr.then
if (onSuccess && onSuccess.call) {
a = [,onSuccess, onError];
}
// Test if there's a timeout error
e && a[2] && a[2](e, xhr)
// Test if request is complete
if (xhr.readyState == 4) {
// index will be:
// 0 if undefined
// 1 if status is between 200 and 399
// 2 if status is over
cb = a[0|xhr.status / 200];
if (cb) {
/*try {
data = JSON.parse(xhr.responseText) // NICKY FIX -- don't be helpful
} catch (e) {*/
data = null;
//}
cb(data, xhr);
}
}
};
// Send the GET request
xhr.send();
// Return request
return xhr;
}

View file

@ -1,2 +0,0 @@
//0.3.5
function pegasus(a,b,c){return c=new XMLHttpRequest,c.open("GET",a),a=[],pegasus.timeout&&(c.timeout=pegasus.timeout),c.ontimeout=function(a){b=a},c.onreadystatechange=c.then=function(d,e,f,g){if(d&&d.call&&(a=[,d,e]),b&&a[2]&&a[2](b,c),4==c.readyState&&(f=a[0|c.status/200])){try{g=JSON.parse(c.responseText)}catch(a){g=null}f(g,c)}},c.send(),c}

View file

@ -81,7 +81,7 @@
var hasStacks = false;
try {
throw new Error();
// throw new Error(); // NICKY FIX -- STOP TRYING TO BE HELPFUL
} catch (e) {
hasStacks = !!e.stack;
}
@ -461,7 +461,7 @@ function captureLine() {
}
try {
throw new Error();
// throw new Error(); // NICKY FIX -- STOP TRYING TO BE HELPFUL
} catch (e) {
var lines = e.stack.split("\n");
var firstLine = lines[0].indexOf("@") > 0 ? lines[1] : lines[2];

View file

@ -1,107 +0,0 @@
/*var slides = [
// SIM
{
id: "sim",
add:[
{id:"tournament", type:"Tournament", x:0, y:20},
{
id:"_w1", type:"WordBox",
x:500, y:0, width:460, height:50,
text:"Let's say there are three kinds of players:<br>"+
"<span style='color:#FF75FF;'>Always Cooperate</span>, "+
"<span style='color:#52537F;'>Always Cheat</span> & "+
"<span style='color:#4089DD;'>Tit For Tat</span>"+
"<br><br>"+
"What happens when you let a mixed population play against each other, and evolve over time?"
},
{
id:"_b1", type:"Button",
x:500, y:150, width:140,
text:"1) play tournament",
message:"tournament/play"
},
{
id:"_b2", type:"Button",
x:500, y:220, width:140,
text:"2) eliminate bottom 5",
message:"tournament/eliminate",
active:false
},
{
id:"_b3", type:"Button",
x:500, y:290, width:140,
text:"3) reproduce top 5",
message:"tournament/reproduce",
active:false
},
{
id:"_w3", type:"WordBox",
x:500, y:370, width:460, height:200,
text:"Always Cheat dominates at first, but when it runs out of suckers to exploit, "+
"its empire collapses and the fairer Tit For Tat takes over.<br>"+
"<br>"+
"<i>We are not punished for our sins, but by them.</i><br>"+
"- Elbert Hubbard"
}
]
},
// Intro
{
id: "intro0",
add:[
{id:"button", type:"Button", x:550, y:200, width:100, height:100, text:"NEXT SLIDE", message:"slideshow/next"},
]
},
// Intro 1
{
id: "intro1",
add:[
{id:"wordbox1", type:"WordBox", x:500, y:0, width:100, height:200, text:"foo bar <b>foo bar</b> <i>foo<i> bar"},
]
},
// Intro 2
{
id: "intro2",
add:[
{id:"wordbox2", type:"WordBox", x:500, y:100, width:100, height:200, text:"even more foo bar"},
{id:"silly", type:"SillyPixi", x:700, y:50, width:200, height:200}
]
},
// Intro 3
{
id: "intro3",
remove:[
{id:"wordbox1"},
{id:"wordbox2"}
],
add:[
{id:"wordbox3", type:"WordBox", x:500, y:0, width:100, height:200, text:"aaAAAAHHHHhhh"}
]
},
// Intro 4
{
id: "intro4",
remove:[
{id:"wordbox3"}
]
},
// Intro 5
{
id: "intro5",
remove:[
{id:"button"},
{id:"silly"}
],
add:[
{id:"the_end", type:"WordBox", x:600, y:300, width:100, height:200, text:"THE END"}
]
}
];*/

View file

@ -11,7 +11,7 @@ PD.PAYOFFS_DEFAULT = {
PD.PAYOFFS = PD.PAYOFFS_DEFAULT;
PD.NOISE = 0.05;
PD.NOISE = 0;
PD.getPayoffs = function(move1, move2){
var payoffs = PD.PAYOFFS;

108
js/sims/SandboxUI.js Normal file
View file

@ -0,0 +1,108 @@
function SandboxUI(config){
var self = this;
self.id = config.id;
// Create DOM
self.dom = document.createElement("div");
self.dom.className = "object";
var dom = self.dom;
/////////////////////////////////////////
// BUTTONS for playing //////////////////
/////////////////////////////////////////
var playButton = new Button({x:130, y:135, text_id:"label_play", message:"tournament/autoplay"});
dom.appendChild(playButton.dom);
var stepButton = new Button({x:130, y:135+70, text_id:"label_step", message:"tournament/step"});
dom.appendChild(stepButton.dom);
var resetButton = new Button({x:130, y:135+70*2, text_id:"label_reset", message:"tournament/reset"});
dom.appendChild(resetButton.dom);
/////////////////////////////////////////
// Create TABS & PAGES //////////////////
/////////////////////////////////////////
// Tabs
var tabs = document.createElement("div");
tabs.id = "sandbox_tabs";
dom.appendChild(tabs);
// Tab Hitboxes
var _makeHitbox = function(label, x, width, pageIndex){
label = label.toUpperCase();
var hitbox = document.createElement("div");
hitbox.className = "hitbox";
hitbox.style.left = x+"px";
hitbox.style.width = width+"px";
hitbox.innerHTML = label;
tabs.appendChild(hitbox);
(function(pageIndex){
hitbox.onclick = function(){
_goToPage(pageIndex);
};
})(pageIndex);
};
_makeHitbox(Words.get("label_population"), 30, 100, 0);
_makeHitbox(Words.get("label_payoffs"), 220, 100, 1);
_makeHitbox(Words.get("label_rules"), 366, 100, 2);
// Pages
var pages = [];
var _makePage = function(){
var page = document.createElement("div");
page.className = "sandbox_page";
tabs.appendChild(page);
pages.push(page);
};
for(var i=0; i<3; i++) _makePage(); // make three pages
// Go To Page
var _goToPage = function(showIndex){
// Background
tabs.style.backgroundPosition = (-showIndex*500)+"px 0px";
// Show page
for(var i=0; i<pages.length; i++) pages[i].style.display = "none";
pages[showIndex].style.display = "block";
};
_goToPage(0);
/////////////////////////////////////////
// PAGE 0: POPULATION ///////////////////
/////////////////////////////////////////
/////////////////////////////////////////
// PAGE 1: PAYOFFS //////////////////////
/////////////////////////////////////////
var page = pages[1];
var label = _makeLabel("sandbox_payoffs", 0, 0, 433);
page.appendChild(label);
/////////////////////////////////////////
// PAGE 2: RULES ////////////////////////
/////////////////////////////////////////
/////////////////////////////////////////
// Add & Remove Object //////////////////
/////////////////////////////////////////
// Add...
self.add = function(INSTANT){
return _add(self);
};
// Remove...
self.remove = function(INSTANT){
return _remove(self);
};
}

View file

@ -8,17 +8,20 @@ Tournament.NUM_TURNS = 10;
{strategy:"grudge", count:0},
{strategy:"tft", count:5},
];*/
Tournament.AGENTS = [
{strategy:"all_c", count:13}, // OH THAT'S SO COOL. Mostly C: Pavlov wins, Mostly D: tit for two tats wins (with 5% mistake!)
//{strategy:"all_d", count:13},
{strategy:"tft", count:3},
Tournament.INITIAL_AGENTS = [
{strategy:"all_c", count:15},
{strategy:"all_d", count:5},
{strategy:"tft", count:5},
//{strategy:"grudge", count:3},
//{strategy:"prober", count:6},
{strategy:"tf2t", count:3},
{strategy:"pavlov", count:3},
{strategy:"random", count:3}
//{strategy:"tf2t", count:8},
//{strategy:"pavlov", count:3},
//{strategy:"random", count:3}
];
// OH THAT'S SO COOL. Mostly C: Pavlov wins, Mostly D: tit for two tats wins (with 5% mistake!)
// ALSO, NOISE: tft vs all_d. no random: tft wins. low random: tf2t wins. high random: all_d wins. totally random: nobody wins
//////////////////////////////////////////////
//////////////////////////////////////////////
@ -57,8 +60,8 @@ function Tournament(config){
var _convertCountToArray = function(countList){
var array = [];
for(var i=0; i<Tournament.AGENTS.length; i++){
var A = Tournament.AGENTS[i];
for(var i=0; i<AGENTS.length; i++){
var A = AGENTS[i];
var strategy = A.strategy;
var count = A.count;
for(var j=0; j<count; j++){
@ -82,7 +85,7 @@ function Tournament(config){
self.agentsContainer.removeChildren();
// Convert to an array
self.agents = _convertCountToArray(Tournament.AGENTS);
self.agents = _convertCountToArray(AGENTS);
// Put 'em in a ring
var count = 0;
@ -110,7 +113,6 @@ function Tournament(config){
return a.y - b.y;
});
};
self.populateAgents();
self.createNetwork = function(){
@ -136,7 +138,23 @@ function Tournament(config){
}
};
///////////////////////
// RESET //////////////
///////////////////////
var AGENTS;
self.reset = function(){
AGENTS = JSON.parse(JSON.stringify(Tournament.INITIAL_AGENTS));
self.populateAgents();
self.createNetwork();
self.isAutoPlaying = false;
};
subscribe("tournament/reset", self.reset);
self.reset();
////////////////////////////////////
// EVOLUTION ///////////////////////
@ -159,10 +177,10 @@ function Tournament(config){
// The worst X
var worst = self.agentsSorted.slice(0,X);
// For each one, subtract from Tournament.AGENTS count, and KILL.
// For each one, subtract from AGENTS count, and KILL.
for(var i=0; i<worst.length; i++){
var badAgent = worst[i];
var config = Tournament.AGENTS.find(function(config){
var config = AGENTS.find(function(config){
return config.strategy==badAgent.strategyName;
});
config.count--; // remove one
@ -181,10 +199,10 @@ function Tournament(config){
// The top X
var best = self.agentsSorted.slice(self.agentsSorted.length-X, self.agentsSorted.length);
// For each one, add to Tournament.AGENTS count
// For each one, add to AGENTS count
for(var i=0; i<best.length; i++){
var goodAgent = best[i];
var config = Tournament.AGENTS.find(function(config){
var config = AGENTS.find(function(config){
return config.strategy==goodAgent.strategyName;
});
config.count++; // ADD one
@ -227,15 +245,27 @@ function Tournament(config){
var STAGE_REPRODUCE = 3;
self.STAGE = STAGE_REST;
/*
self.ALL_AT_ONCE = function(){
publish("tournament/play");
setTimeout(function(){ publish("tournament/eliminate"); },500);
setTimeout(function(){ publish("tournament/reproduce"); },1000);
setTimeout(self.ALL_AT_ONCE, 1500);
// AUTOPLAY
self.isAutoPlaying = false;
var _step = 0;
var _nextStep = function(){
if(_step==0) publish("tournament/play");
if(_step==1) publish("tournament/eliminate");
if(_step==2) publish("tournament/reproduce");
_step = (_step+1)%3;
};
setTimeout(self.ALL_AT_ONCE, 100);
*/
var _startAutoPlay = function(){
self.isAutoPlaying = true;
_nextStep();
setTimeout(function(){
if(self.isAutoPlaying) _startAutoPlay();
},500);
};
subscribe("tournament/autoplay", _startAutoPlay);
subscribe("tournament/step", function(){
self.isAutoPlaying = false;
_nextStep();
});
// ANIMATE
var _playIndex = 0;
@ -252,7 +282,7 @@ function Tournament(config){
self.playOneTournament(); // FOR REAL, NOW.
_playIndex = 0;
self.STAGE = STAGE_REST;
slideshow.objects._b2.activate(); // activate NEXT button!
// slideshow.objects._b2.activate(); // activate NEXT button!
}
}
@ -260,7 +290,7 @@ function Tournament(config){
if(self.STAGE == STAGE_ELIMINATE){
self.eliminateBottom(Tournament.SELECTION);
self.STAGE = STAGE_REST;
slideshow.objects._b3.activate(); // activate NEXT button!
// slideshow.objects._b3.activate(); // activate NEXT button!
}
// REPRODUCE!
@ -285,7 +315,7 @@ function Tournament(config){
if(_tweenTimer>=1){
_tweenTimer = 0;
self.STAGE = STAGE_REST;
slideshow.objects._b1.activate(); // activate NEXT button!
// slideshow.objects._b1.activate(); // activate NEXT button!
}
}
@ -294,23 +324,23 @@ function Tournament(config){
// PLAY A TOURNAMENT
self.deactivateAllButtons = function(){
slideshow.objects._b1.deactivate();
slideshow.objects._b2.deactivate();
slideshow.objects._b3.deactivate();
// slideshow.objects._b1.deactivate();
// slideshow.objects._b2.deactivate();
// slideshow.objects._b3.deactivate();
};
self._startPlay = function(){
self.STAGE=STAGE_PLAY;
self.deactivateAllButtons();
// self.deactivateAllButtons();
};
subscribe("tournament/play", self._startPlay);
self._startEliminate = function(){
self.STAGE=STAGE_ELIMINATE;
self.deactivateAllButtons();
// self.deactivateAllButtons();
};
subscribe("tournament/eliminate", self._startEliminate);
self._startReproduce = function(){
self.STAGE=STAGE_REPRODUCE;
self.deactivateAllButtons();
// self.deactivateAllButtons();
};
subscribe("tournament/reproduce", self._startReproduce);
@ -324,6 +354,9 @@ function Tournament(config){
return _remove(self);
};
// TODO: KILL ALL LISTENERS, TOO.
// TODO: Don't screw up when paused or looking at new tab
}
///////////////////////////////////////////////////////
@ -386,7 +419,7 @@ function TournamentConnection(config){
self.kill = function(){
if(self.IS_DEAD) return;
self.IS_DEAD = true;
self.graphics.parent.removeChild(self.graphics); // remove self's graphics
if(self.graphics.parent) self.graphics.parent.removeChild(self.graphics); // remove self's graphics
};
};
@ -506,7 +539,7 @@ function TournamentAgent(config){
}, 300, Ease.circOut).call(function(){
// NOW remove graphics.
self.graphics.parent.removeChild(self.graphics);
if(self.graphics.parent) self.graphics.parent.removeChild(self.graphics);
// AND remove self from tournament
self.tournament.actuallyRemoveAgent(self);

View file

@ -1,39 +0,0 @@
SLIDES.push({
id: "sim",
add:[
// The tournament simulation
{id:"tournament", type:"Tournament", x:0, y:20},
// All the words!
{
id:"textbox", type:"TextBox",
boxes:[
{ x:500, y:0, width:460, height:50, text_id:"sandbox_1" },
{ x:500, y:370, width:460, height:200, text_id:"sandbox_2" }
]
},
// Buttons
{
id:"_b1", type:"Button", x:500, y:150, width:140,
text_id: "label_play_tournament",
message: "tournament/play"
},
{
id:"_b2", type:"Button", x:500, y:220, width:140,
text_id: "label_eliminate_bottom_5",
message: "tournament/eliminate",
active:false
},
{
id:"_b3", type:"Button", x:500, y:290, width:140,
text_id: "label_reproduce_top_5",
message: "tournament/reproduce",
active:false
}
]
});

View file

@ -0,0 +1,14 @@
SLIDES.push({
id: "sim",
add:[
// The tournament simulation
{id:"tournament", type:"Tournament", x:-20, y:-20},
// Screw it, just ALL of the Sandbox UI
{id:"sandbox", type:"SandboxUI"}
]
});

View file

@ -1,5 +1,152 @@
<!-- - - - - - - - - - - - - - - - - -->
<!-- - - - - - SANDBOX! - - - - - - - -->
<!-- - - - - - - - - - - - - - - - - -->
<!-- SANDBOX! -->
<p id="sandbox_population">
Start the simulation with this distribution of players:
</p>
<p id="sandbox_payoffs">
The payoffs in a one-on-one game are:
</p>
<!--
When translating the following, keep the "[N]", with square brackets,
as a placeholder for the number. Some of these need double-translations,
one for the plural version, one for the singular version.
-->
<p id="sandbox_rules_1">
Play [N] rounds per one-on-one game
</p>
<p id="sandbox_rules_1_single">
Play [N] round per one-on-one game
</p>
<p id="sandbox_rules_2">
After each tournament, eliminate the bottom [N] players &amp; reproduce the top [N] players
</p>
<p id="sandbox_rules_2_single">
After each tournament, eliminate the bottom [N] player &amp; reproduce the top [N] player
</p>
<p id="sandbox_rules_3">
In a one-on-one game, there's a [N]% chance in each round that a player will make a mistake
</p>
<!-- - - - - - - - - - - - - - - - - -->
<!-- - - - - THE PLAYERS - - - - - - -->
<!-- - - - - - - - - - - - - - - - - -->
<p id="label_tft">
Tit For Tat
</p>
<p id="desc_tft">
I Cooperate on the first round.
Then, I just do whatever you did the last round.
If you Cheat me, I'll Cheat you back &em;
but if you Cooperate, I'll forgive you immediately!
</p>
<p id="label_all_d">
Always Cheat
</p>
<p id="desc_all_d">
Ain't I a stinker?
</p>
<p id="label_all_c">
Always Cooperate
</p>
<p id="desc_all_c">
💖 💖 💖
</p>
<p id="label_grudge">
Grudger
</p>
<p id="desc_grudge">
I'll always Cooperate... until you Cheat me even once.
Then, I'll <i>always</i> Cheat you back. NO FORGIVENESS.
</p>
<p id="label_prober">
Prober
</p>
<p id="desc_prober">
First: I analyze you.
I start: Cooperate, Cheat, Cooperate, Cooperate.
Then: if you retaliated with a Cheat, I switch to playing Tit For Tat.
But: if you never fight back, I Cheat the heck out of you.
My dear Watson: elementary.
</p>
<p id="label_tf2t">
Tit For Two Tats
</p>
<p id="desc_tf2t">
I Cooperate on the first round.
After that, if you Cheat me... well, I'll forgive you once.
However, if you Cheat me twice in a row, <i>then</i> I'll Cheat back.
(But again, if you Cooperate, I'll forgive you immediately!)
</p>
<p id="label_pavlov">
Pavlov's Dog
</p>
<p id="desc_pavlov">
I Cooperate on the first round.
After that, if you Cooperated in the previous round,
I'll just do what I did last time (even if it was a mistake).
But if you Cheated in the previous round,
I'll do the <i>opposite</i> of what I did last time (even if it was a mistake).
</p>
<p id="label_random">
Lol So Random
</p>
<p id="desc_random">
monkey tacos! robot ninja bacon pirate!
i randomly play Cheat or Cooperate coz lol i'm so random
</p>
<!-- - - - - - - - - - - - - - - - - -->
<!-- - - - - SMALL LABELS! - - - - - -->
<!-- - - - - - - - - - - - - - - - - -->
<p id="label_cooperate">
cooperate
</p>
<p id="label_cheat">
cheat
</p>
<p id="label_play">
play
</p>
<p id="label_step">
step
</p>
<p id="label_reset">
reset
</p>
<p id="label_population">
population
</p>
<p id="label_payoffs">
payoffs
</p>
<p id="label_rules">
rules
</p>
<p id="label_play_tournament">
1) play tournament
@ -12,21 +159,3 @@
<p id="label_reproduce_top_5">
3) reproduce top 5
</p>
<p id="sandbox_1">
Let's say there are three kinds of players:
<br>
<span style='color:#FF75FF;'>Always Cooperate</span>,
<span style='color:#52537F;'>Always Cheat</span> &amp;
<span style='color:#4089DD;'>Tit For Tat</span>
<br><br>
"What happens when you let a mixed population play against each other, and evolve over time?
</p>
<p id="sandbox_2">
Always Cheat dominates at first, but when it runs out of suckers to exploit,
its empire collapses and the fairer Tit For Tat takes over.
<br><br>
<i>We are not punished for our sins, but by them.</i><br>
- Elbert Hubbard
</p>