payoffs and playing sim
This commit is contained in:
parent
e8c18fdbe9
commit
f918fe2275
12 changed files with 312 additions and 45 deletions
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
assets/sandbox_incdec.png
Normal file
BIN
assets/sandbox_incdec.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 51 KiB |
|
@ -129,6 +129,12 @@ body{
|
|||
background: url(../assets/sandbox_tabs.png);
|
||||
width:500px; height:470px;
|
||||
background-size: auto 100%;
|
||||
|
||||
-webkit-user-select: none; /* Chrome all / Safari all */
|
||||
-moz-user-select: none; /* Firefox all */
|
||||
-ms-user-select: none; /* IE 10+ */
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
#sandbox_tabs > div{
|
||||
position: absolute;
|
||||
|
@ -144,6 +150,33 @@ body{
|
|||
left: 33px;
|
||||
top: 80px;
|
||||
}
|
||||
.incdec{
|
||||
width: 0; height: 0;
|
||||
position: absolute;
|
||||
}
|
||||
.incdec > div{
|
||||
position: absolute;
|
||||
}
|
||||
.incdec > .incdec_num{
|
||||
width:50px; height:50px;
|
||||
font-size: 25px;
|
||||
text-align: center;
|
||||
top: -16px;
|
||||
left: -25px;
|
||||
cursor: default;
|
||||
}
|
||||
.incdec > .incdec_control{
|
||||
left:-10px;
|
||||
width:20px; height:20px;
|
||||
background: url(../assets/sandbox_incdec.png);
|
||||
background-size: auto 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.incdec > .incdec_control[arrow=up]{ top:-35px; background-position:0px 0px; }
|
||||
.incdec > .incdec_control[arrow=up]:hover{ background-position:-20px 0px; }
|
||||
.incdec > .incdec_control[arrow=down]{ bottom:-35px; background-position:-40px 0px; }
|
||||
.incdec > .incdec_control[arrow=down]:hover{ background-position:-60px 0px; }
|
||||
|
||||
|
||||
/*************************/
|
||||
/***** SLIDE SELECT ******/
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<script src="js/lib/pixi.min.js"></script>
|
||||
<script>var createjs = window;</script>
|
||||
<script src="js/lib/tweenjs-0.6.2.min.js"></script>
|
||||
<script>Ticker.framerate=60;</script>
|
||||
<script>Ticker.framerate=60; Ticker.paused=true;</script>
|
||||
|
||||
<!-- Core Engine -->
|
||||
<script src="js/core/Loader.js"></script>
|
||||
|
@ -31,6 +31,7 @@
|
|||
<script src="js/core/Button.js"></script>
|
||||
<script src="js/core/TextBox.js"></script>
|
||||
<script src="js/core/Words.js"></script>
|
||||
<script src="js/core/IncDecNumber.js"></script>
|
||||
|
||||
<!-- Simulations -->
|
||||
<script src="js/sims/PD.js"></script>
|
||||
|
|
|
@ -25,9 +25,12 @@ function Button(config){
|
|||
button.style.left = config.x+"px";
|
||||
button.style.top = config.y+"px";
|
||||
config.upperCase = (config.upperCase===undefined) ? true : config.upperCase;
|
||||
var words = Words.get(config.text_id);
|
||||
if(config.upperCase) words=words.toUpperCase();
|
||||
text.innerHTML = words;
|
||||
self.setText = function(text_id){
|
||||
var words = Words.get(text_id);
|
||||
if(config.upperCase) words=words.toUpperCase();
|
||||
text.innerHTML = words;
|
||||
};
|
||||
self.setText(config.text_id);
|
||||
|
||||
// On hover...
|
||||
hitbox.onmouseover = function(){
|
||||
|
@ -39,7 +42,10 @@ function Button(config){
|
|||
|
||||
// On click...
|
||||
hitbox.onclick = function(){
|
||||
if(self.active) publish(config.message);
|
||||
if(self.active){
|
||||
if(config.onclick) config.onclick();
|
||||
if(config.message) publish(config.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Activate/Deactivate
|
||||
|
|
81
js/core/IncDecNumber.js
Normal file
81
js/core/IncDecNumber.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*****************************
|
||||
|
||||
{
|
||||
x:x, y:y, max:5, min:-5,
|
||||
value: PD.PAYOFFS_DEFAULT[letter],
|
||||
onchange: function(value){
|
||||
publish("pd/editPayoffs/"+letter,[value]);
|
||||
}
|
||||
}
|
||||
|
||||
*****************************/
|
||||
function IncDecNumber(config){
|
||||
|
||||
var self = this;
|
||||
self.id = config.id;
|
||||
|
||||
// Properties
|
||||
self.value = config.value;
|
||||
|
||||
// Create DOM
|
||||
var dom = document.createElement("div");
|
||||
dom.className = "incdec";
|
||||
dom.style.left = config.x+"px";
|
||||
dom.style.top = config.y+"px";
|
||||
self.dom = dom;
|
||||
|
||||
// Number
|
||||
var num = document.createElement("div");
|
||||
num.className = "incdec_num";
|
||||
dom.appendChild(num);
|
||||
self.setValue = function(value){
|
||||
|
||||
// Bounds
|
||||
if(value>config.max) value=config.max;
|
||||
if(value<config.min) value=config.min;
|
||||
|
||||
// Value & UI
|
||||
self.value = value;
|
||||
num.innerHTML = self.value;
|
||||
|
||||
};
|
||||
self.setValue(config.value);
|
||||
|
||||
// Two buttons
|
||||
var up = document.createElement("div");
|
||||
up.className = "incdec_control";
|
||||
up.setAttribute("arrow","up");
|
||||
up.onclick = function(){
|
||||
self.setValue(self.value+1);
|
||||
self.onchange(self.value);
|
||||
};
|
||||
dom.appendChild(up);
|
||||
|
||||
var down = document.createElement("div");
|
||||
down.className = "incdec_control";
|
||||
down.setAttribute("arrow","down");
|
||||
down.onclick = function(){
|
||||
self.setValue(self.value-1);
|
||||
self.onchange(self.value);
|
||||
};
|
||||
dom.appendChild(down);
|
||||
|
||||
// On Change...
|
||||
self.onchange = function(value){
|
||||
config.onchange(value);
|
||||
};
|
||||
|
||||
///////////////////////////////////////
|
||||
///////////////////////////////////////
|
||||
|
||||
// Add...
|
||||
self.add = function(INSTANT){
|
||||
return _addFade(self, INSTANT);
|
||||
};
|
||||
|
||||
// Remove...
|
||||
self.remove = function(INSTANT){
|
||||
return _removeFade(self, INSTANT);
|
||||
};
|
||||
|
||||
}
|
|
@ -47,15 +47,33 @@ var _removeFade = function(self, INSTANT){
|
|||
};
|
||||
|
||||
// Make Label
|
||||
var _makeLabel = function(wordID, x, y, width, height){
|
||||
var _makeLabel = function(wordID, config){
|
||||
|
||||
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);
|
||||
config = config || {};
|
||||
|
||||
if(config.x!==undefined) dom.style.left = config.x+"px";
|
||||
if(config.y!==undefined) dom.style.top = config.y+"px";
|
||||
if(config.w!==undefined) dom.style.width = config.w+"px";
|
||||
if(config.h!==undefined) dom.style.height = config.h+"px";
|
||||
|
||||
if(config.rotation!==undefined) dom.style.transform = "rotate("+config.rotation+"deg)";
|
||||
if(config.align!==undefined) dom.style.textAlign = config.align;
|
||||
if(config.color!==undefined) dom.style.color = config.color;
|
||||
|
||||
return dom;
|
||||
|
||||
};
|
||||
|
||||
// Tween
|
||||
var Tween_get = function(target){
|
||||
return Tween.get(target, {useTicks:true});
|
||||
}
|
||||
var _s = function(seconds){
|
||||
return Math.ceil(Ticker.framerate*seconds); // converts seconds to ticks
|
||||
};
|
||||
|
||||
/*******
|
||||
|
|
|
@ -9,7 +9,25 @@ PD.PAYOFFS_DEFAULT = {
|
|||
T: 3 // temptation: you put no coin, got 3 coins anyway
|
||||
};
|
||||
|
||||
PD.PAYOFFS = PD.PAYOFFS_DEFAULT;
|
||||
PD.PAYOFFS = JSON.parse(JSON.stringify(PD.PAYOFFS_DEFAULT));
|
||||
|
||||
subscribe("pd/editPayoffs", function(payoffs){
|
||||
PD.PAYOFFS = payoffs;
|
||||
});
|
||||
subscribe("pd/editPayoffs/P", function(value){ PD.PAYOFFS.P = value; });
|
||||
subscribe("pd/editPayoffs/S", function(value){ PD.PAYOFFS.S = value; });
|
||||
subscribe("pd/editPayoffs/R", function(value){ PD.PAYOFFS.R = value; });
|
||||
subscribe("pd/editPayoffs/T", function(value){ PD.PAYOFFS.T = value; });
|
||||
subscribe("pd/defaultPayoffs", function(){
|
||||
|
||||
PD.PAYOFFS = JSON.parse(JSON.stringify(PD.PAYOFFS_DEFAULT));
|
||||
|
||||
publish("pd/editPayoffs/P", [PD.PAYOFFS.P]);
|
||||
publish("pd/editPayoffs/S", [PD.PAYOFFS.S]);
|
||||
publish("pd/editPayoffs/R", [PD.PAYOFFS.R]);
|
||||
publish("pd/editPayoffs/T", [PD.PAYOFFS.T]);
|
||||
|
||||
});
|
||||
|
||||
PD.NOISE = 0;
|
||||
|
||||
|
|
|
@ -12,10 +12,29 @@ function SandboxUI(config){
|
|||
// BUTTONS for playing //////////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
var playButton = new Button({x:130, y:135, text_id:"label_play", message:"tournament/autoplay"});
|
||||
var playButton = new Button({
|
||||
x:130, y:135, text_id:"label_play",
|
||||
onclick: function(){
|
||||
if(slideshow.objects.tournament.isAutoPlaying){
|
||||
publish("tournament/autoplay/stop");
|
||||
}else{
|
||||
publish("tournament/autoplay/start");
|
||||
}
|
||||
}
|
||||
});
|
||||
subscribe("tournament/autoplay/stop",function(){
|
||||
playButton.setText("label_play");
|
||||
});
|
||||
subscribe("tournament/autoplay/start",function(){
|
||||
playButton.setText("label_stop");
|
||||
});
|
||||
dom.appendChild(playButton.dom);
|
||||
var stepButton = new Button({x:130, y:135+70, text_id:"label_step", message:"tournament/step"});
|
||||
|
||||
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);
|
||||
|
||||
|
@ -84,13 +103,62 @@ function SandboxUI(config){
|
|||
|
||||
var page = pages[1];
|
||||
|
||||
var label = _makeLabel("sandbox_payoffs", 0, 0, 433);
|
||||
page.appendChild(label);
|
||||
// Labels
|
||||
page.appendChild(_makeLabel("sandbox_payoffs", {x:0, y:0, w:433}));
|
||||
page.appendChild(_makeLabel("label_cooperate", {x:212, y:64, rotation:45, align:"center", color:"#cccccc"}));
|
||||
page.appendChild(_makeLabel("label_cooperate", {x:116, y:64, rotation:-45, align:"center", color:"#cccccc"}));
|
||||
page.appendChild(_makeLabel("label_cheat", {x:309, y:137, rotation:45, align:"center", color:"#cccccc"}));
|
||||
page.appendChild(_makeLabel("label_cheat", {x:70, y:137, rotation:-45, align:"center", color:"#cccccc"}));
|
||||
|
||||
// Inc(rement) De(crement) Numbers
|
||||
// which are symmetrical, and update each other!
|
||||
var numbers = [];
|
||||
var _makeIncDec = function(letter,x,y){
|
||||
(function(letter,x,y){
|
||||
|
||||
var number = new IncDecNumber({
|
||||
x:x, y:y, max:5, min:-5,
|
||||
value: PD.PAYOFFS_DEFAULT[letter],
|
||||
onchange: function(value){
|
||||
publish("pd/editPayoffs/"+letter,[value]);
|
||||
}
|
||||
});
|
||||
subscribe("pd/editPayoffs/"+letter,function(value){
|
||||
number.setValue(value);
|
||||
});
|
||||
page.appendChild(number.dom);
|
||||
numbers.push(number);
|
||||
|
||||
})(letter,x,y);
|
||||
};
|
||||
|
||||
_makeIncDec("R", 191, 127);
|
||||
_makeIncDec("R", 233, 127);
|
||||
|
||||
_makeIncDec("T", 121, 197);
|
||||
_makeIncDec("S", 161, 197);
|
||||
|
||||
_makeIncDec("S", 263, 197);
|
||||
_makeIncDec("T", 306, 197);
|
||||
|
||||
_makeIncDec("P", 192, 268);
|
||||
_makeIncDec("P", 232, 268);
|
||||
|
||||
// Reset
|
||||
var resetPayoffs = new Button({x:240, y:300, text_id:"sandbox_reset_payoffs", message:"pd/defaultPayoffs"});
|
||||
page.appendChild(resetPayoffs.dom);
|
||||
|
||||
/////////////////////////////////////////
|
||||
// PAGE 2: RULES ////////////////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
var page = pages[2];
|
||||
|
||||
// Labels
|
||||
page.appendChild(_makeLabel("sandbox_rules_1", {x:0, y:0, w:433}));
|
||||
page.appendChild(_makeLabel("sandbox_rules_2", {x:0, y:100, w:433}));
|
||||
page.appendChild(_makeLabel("sandbox_rules_3", {x:0, y:225, w:433}));
|
||||
|
||||
/////////////////////////////////////////
|
||||
// Add & Remove Object //////////////////
|
||||
/////////////////////////////////////////
|
||||
|
|
|
@ -82,7 +82,7 @@ function Tournament(config){
|
|||
self.populateAgents = function(){
|
||||
|
||||
// Clear EVERYTHING
|
||||
self.agentsContainer.removeChildren();
|
||||
while(self.agents.length>0) self.agents[0].kill();
|
||||
|
||||
// Convert to an array
|
||||
self.agents = _convertCountToArray(AGENTS);
|
||||
|
@ -117,11 +117,7 @@ function Tournament(config){
|
|||
self.createNetwork = function(){
|
||||
|
||||
// Clear EVERYTHING
|
||||
self.connections = [];
|
||||
self.networkContainer.removeChildren();
|
||||
for(var i=0; i<self.agents.length; i++){
|
||||
self.agents[i].clearConnections();
|
||||
}
|
||||
while(self.connections.length>0) self.connections[0].kill();
|
||||
|
||||
// Connect all of 'em
|
||||
for(var i=0; i<self.agents.length; i++){
|
||||
|
@ -129,8 +125,9 @@ function Tournament(config){
|
|||
for(var j=i+1; j<self.agents.length; j++){
|
||||
var playerB = self.agents[j];
|
||||
var connection = new TournamentConnection({
|
||||
from:playerA,
|
||||
to:playerB
|
||||
tournament: self,
|
||||
from: playerA,
|
||||
to: playerB
|
||||
});
|
||||
self.networkContainer.addChild(connection.graphics);
|
||||
self.connections.push(connection);
|
||||
|
@ -138,6 +135,10 @@ function Tournament(config){
|
|||
}
|
||||
|
||||
};
|
||||
self.actuallyRemoveConnection = function(connection){
|
||||
var index = self.connections.indexOf(connection);
|
||||
self.connections.splice(index,1);
|
||||
};
|
||||
|
||||
|
||||
///////////////////////
|
||||
|
@ -146,10 +147,21 @@ function Tournament(config){
|
|||
|
||||
var AGENTS;
|
||||
self.reset = function(){
|
||||
|
||||
// Agents & Network...
|
||||
AGENTS = JSON.parse(JSON.stringify(Tournament.INITIAL_AGENTS));
|
||||
self.populateAgents();
|
||||
self.createNetwork();
|
||||
self.isAutoPlaying = false;
|
||||
|
||||
// Animation...
|
||||
self.STAGE = STAGE_REST;
|
||||
_playIndex = 0;
|
||||
_tweenTimer = 0;
|
||||
|
||||
// Stop autoplay!
|
||||
publish("tournament/autoplay/stop");
|
||||
_step = 0;
|
||||
|
||||
};
|
||||
|
||||
subscribe("tournament/reset", self.reset);
|
||||
|
@ -184,7 +196,7 @@ function Tournament(config){
|
|||
return config.strategy==badAgent.strategyName;
|
||||
});
|
||||
config.count--; // remove one
|
||||
badAgent.kill(); // KILL
|
||||
badAgent.eliminate(); // ELIMINATE
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -249,6 +261,7 @@ function Tournament(config){
|
|||
self.isAutoPlaying = false;
|
||||
var _step = 0;
|
||||
var _nextStep = function(){
|
||||
if(self.STAGE!=STAGE_REST) return;
|
||||
if(_step==0) publish("tournament/play");
|
||||
if(_step==1) publish("tournament/eliminate");
|
||||
if(_step==2) publish("tournament/reproduce");
|
||||
|
@ -261,9 +274,13 @@ function Tournament(config){
|
|||
if(self.isAutoPlaying) _startAutoPlay();
|
||||
},500);
|
||||
};
|
||||
subscribe("tournament/autoplay", _startAutoPlay);
|
||||
subscribe("tournament/step", function(){
|
||||
var _stopAutoPlay = function(){
|
||||
self.isAutoPlaying = false;
|
||||
};
|
||||
subscribe("tournament/autoplay/start", _startAutoPlay);
|
||||
subscribe("tournament/autoplay/stop", _stopAutoPlay);
|
||||
subscribe("tournament/step", function(){
|
||||
publish("tournament/autoplay/stop");
|
||||
_nextStep();
|
||||
});
|
||||
|
||||
|
@ -272,6 +289,9 @@ function Tournament(config){
|
|||
var _tweenTimer = 0;
|
||||
app.ticker.add(function(delta) {
|
||||
|
||||
// Tick
|
||||
Tween.tick();
|
||||
|
||||
// PLAY!
|
||||
if(self.STAGE == STAGE_PLAY){
|
||||
if(_playIndex>0) self.agents[_playIndex-1].dehighlightConnections();
|
||||
|
@ -281,6 +301,7 @@ function Tournament(config){
|
|||
}else{
|
||||
self.playOneTournament(); // FOR REAL, NOW.
|
||||
_playIndex = 0;
|
||||
_tweenTimer = 0;
|
||||
self.STAGE = STAGE_REST;
|
||||
// slideshow.objects._b2.activate(); // activate NEXT button!
|
||||
}
|
||||
|
@ -289,7 +310,11 @@ function Tournament(config){
|
|||
// ELIMINATE!
|
||||
if(self.STAGE == STAGE_ELIMINATE){
|
||||
self.eliminateBottom(Tournament.SELECTION);
|
||||
self.STAGE = STAGE_REST;
|
||||
_tweenTimer++;
|
||||
if(_tweenTimer==_s(0.3)){
|
||||
_tweenTimer = 0;
|
||||
self.STAGE = STAGE_REST;
|
||||
}
|
||||
// slideshow.objects._b3.activate(); // activate NEXT button!
|
||||
}
|
||||
|
||||
|
@ -366,6 +391,8 @@ function Tournament(config){
|
|||
function TournamentConnection(config){
|
||||
|
||||
var self = this;
|
||||
self.config = config;
|
||||
self.tournament = config.tournament;
|
||||
|
||||
// Connect from & to
|
||||
self.from = config.from;
|
||||
|
@ -419,7 +446,8 @@ function TournamentConnection(config){
|
|||
self.kill = function(){
|
||||
if(self.IS_DEAD) return;
|
||||
self.IS_DEAD = true;
|
||||
if(self.graphics.parent) self.graphics.parent.removeChild(self.graphics); // remove self's graphics
|
||||
self.graphics.parent.removeChild(self.graphics); // remove self's graphics
|
||||
self.tournament.actuallyRemoveConnection(self);
|
||||
};
|
||||
|
||||
};
|
||||
|
@ -445,6 +473,9 @@ function TournamentAgent(config){
|
|||
for(var i=0;i<self.connections.length;i++) self.connections[i].dehighlight();
|
||||
};
|
||||
self.clearConnections = function(){
|
||||
for(var i=0;i<self.connections.length;i++){
|
||||
self.connections[i].kill();
|
||||
}
|
||||
self.connections = [];
|
||||
};
|
||||
|
||||
|
@ -521,30 +552,34 @@ function TournamentAgent(config){
|
|||
};
|
||||
self.updatePosition();
|
||||
|
||||
// KILL
|
||||
self.kill = function(){
|
||||
|
||||
// KILL ALL CONNECTIONS
|
||||
for(var i=0;i<self.connections.length;i++){
|
||||
self.connections[i].kill();
|
||||
}
|
||||
// ELIMINATE
|
||||
self.eliminate = function(){
|
||||
|
||||
// INSTA-KILL ALL CONNECTIONS
|
||||
self.clearConnections();
|
||||
|
||||
// Tween -- DIE!
|
||||
scoreText.visible = false;
|
||||
Tween.get(g).to({
|
||||
Tween_get(g).to({
|
||||
alpha: 0,
|
||||
x: g.x+Math.random()*20-10,
|
||||
y: g.y+Math.random()*20-10,
|
||||
rotation: Math.random()*0.5-0.25
|
||||
}, 300, Ease.circOut).call(function(){
|
||||
|
||||
// NOW remove graphics.
|
||||
if(self.graphics.parent) self.graphics.parent.removeChild(self.graphics);
|
||||
}, _s(0.3), Ease.circOut).call(self.kill);
|
||||
|
||||
// AND remove self from tournament
|
||||
self.tournament.actuallyRemoveAgent(self);
|
||||
};
|
||||
|
||||
});
|
||||
// KILL (actually insta-remove)
|
||||
self.kill = function(){
|
||||
|
||||
// Remove ANY tweens
|
||||
Tween.removeTweens(g);
|
||||
|
||||
// NOW remove graphics.
|
||||
g.parent.removeChild(g);
|
||||
|
||||
// AND remove self from tournament
|
||||
self.tournament.actuallyRemoveAgent(self);
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -9,6 +9,9 @@ Start the simulation with this distribution of players:
|
|||
<p id="sandbox_payoffs">
|
||||
The payoffs in a one-on-one game are:
|
||||
</p>
|
||||
<p id="sandbox_reset_payoffs">
|
||||
reset payoffs
|
||||
</p>
|
||||
|
||||
<!--
|
||||
When translating the following, keep the "[N]", with square brackets,
|
||||
|
@ -31,7 +34,7 @@ After each tournament, eliminate the bottom [N] player & reproduce the top [
|
|||
</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
|
||||
In each round of a one-on-one game, there's a [N]% chance a player makes a mistake
|
||||
</p>
|
||||
|
||||
|
||||
|
@ -128,6 +131,10 @@ cheat
|
|||
play
|
||||
</p>
|
||||
|
||||
<p id="label_stop">
|
||||
stop
|
||||
</p>
|
||||
|
||||
<p id="label_step">
|
||||
step
|
||||
</p>
|
||||
|
|
Loading…
Reference in a new issue