Large diffs are not rendered by default.

@@ -0,0 +1,129 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="generator" content="pandoc" />
<title></title>
<style type="text/css">code{white-space: pre;}</style>
<style type="text/css">
div.sourceCode { overflow-x: auto; }
table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
margin: 0; padding: 0; vertical-align: baseline; border: none; }
table.sourceCode { width: 100%; line-height: 100%; }
td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
td.sourceCode { padding-left: 5px; }
code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
code > span.dt { color: #902000; } /* DataType */
code > span.dv { color: #40a070; } /* DecVal */
code > span.bn { color: #40a070; } /* BaseN */
code > span.fl { color: #40a070; } /* Float */
code > span.ch { color: #4070a0; } /* Char */
code > span.st { color: #4070a0; } /* String */
code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
code > span.ot { color: #007020; } /* Other */
code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
code > span.fu { color: #06287e; } /* Function */
code > span.er { color: #ff0000; font-weight: bold; } /* Error */
code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
code > span.cn { color: #880000; } /* Constant */
code > span.sc { color: #4070a0; } /* SpecialChar */
code > span.vs { color: #4070a0; } /* VerbatimString */
code > span.ss { color: #bb6688; } /* SpecialString */
code > span.im { } /* Import */
code > span.va { color: #19177c; } /* Variable */
code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code > span.op { color: #666666; } /* Operator */
code > span.bu { } /* BuiltIn */
code > span.ex { } /* Extension */
code > span.pp { color: #bc7a00; } /* Preprocessor */
code > span.at { color: #7d9029; } /* Attribute */
code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
</style>
<link rel="stylesheet" href="docs.css" type="text/css" />
</head>
<body>
<h1 id="tdd-desarrollo-dirigido-por-tests">TDD: Desarrollo dirigido por tests</h1>
<p>Practicando TDD, iremos escribiendo código de forma que los tests pasen. Los tests vienen dados pero están desactivados. La <a href="./GUIDE.md">guía de la práctica</a> recomienda en qué orden activar los tests para completar la práctica poco a poco.</p>
<h3 id="tests-y-suites">Tests y suites</h3>
<p>En esta práctica usamos <a href="http://jasmine.github.io"><strong>Jasmine</strong></a> como framework para tests. En Jasmine escribimos suites y tests. Las suites se pueden anidar y pueden llevar código de inicialización. En general, la API de Jasmine es muy clara y no necesita mayor explicación. De todas formas, aquí tienes un ejemplo:</p>
<div class="sourceCode"><pre class="sourceCode js"><code class="sourceCode javascript"><span class="at">describe</span>(<span class="st">&#39;Los suites en Jasmine&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>

<span class="at">describe</span>(<span class="st">&#39;pueden anidarse&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>

<span class="at">it</span>(<span class="st">&#39;y encierran tests con expectativas&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>
<span class="at">expect</span>(<span class="dv">2</span> <span class="op">+</span> <span class="dv">2</span>).<span class="at">toBe</span>(<span class="dv">4</span>)<span class="op">;</span>
<span class="op">}</span>)<span class="op">;</span>

<span class="op">}</span>)<span class="op">;</span>

<span class="op">}</span>)<span class="op">;</span></code></pre></div>
<p>Se llama test a un fragmento de código que pone a prueba una funcionalidad específica. El test puede pasar o fallar. En caso de fallo, la consola mostrará por qué ha fallado en la forma de una traza:</p>
<div class="sourceCode"><pre class="sourceCode js"><code class="sourceCode javascript"><span class="fl">15.2</span>) Expected <span class="st">&#39;b&#39;</span> to be <span class="st">&#39;c&#39;</span>.
Error<span class="op">:</span> Expected <span class="st">&#39;b&#39;</span> to be <span class="st">&#39;c&#39;</span>.
at <span class="at">stack</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/node_modules/jasmine<span class="op">-</span>core/lib/jasmine<span class="op">-</span>core/<span class="va">jasmine</span>.<span class="at">js</span><span class="op">:</span><span class="dv">1640</span><span class="op">:</span><span class="dv">17</span>)
at <span class="at">buildExpectationResult</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/node_modules/jasmine<span class="op">-</span>core/lib/jasmine<span class="op">-</span>core/<span class="va">jasmine</span>.<span class="at">js</span><span class="op">:</span><span class="dv">1610</span><span class="op">:</span><span class="dv">14</span>)
at <span class="va">Spec</span>.<span class="at">expectationResultFactory</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/node_modules/jasmine<span class="op">-</span>core/lib/jasmine<span class="op">-</span>core/<span class="va">jasmine</span>.<span class="at">js</span><span class="op">:</span><span class="dv">655</span><span class="op">:</span><span class="dv">18</span>)
at <span class="va">Spec</span>.<span class="at">addExpectationResult</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/node_modules/jasmine<span class="op">-</span>core/lib/jasmine<span class="op">-</span>core/<span class="va">jasmine</span>.<span class="at">js</span><span class="op">:</span><span class="dv">342</span><span class="op">:</span><span class="dv">34</span>)
at <span class="va">Expectation</span>.<span class="at">addExpectationResult</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/node_modules/jasmine<span class="op">-</span>core/lib/jasmine<span class="op">-</span>core/<span class="va">jasmine</span>.<span class="at">js</span><span class="op">:</span><span class="dv">599</span><span class="op">:</span><span class="dv">21</span>)
at <span class="va">Expectation</span>.<span class="at">toBe</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/node_modules/jasmine<span class="op">-</span>core/lib/jasmine<span class="op">-</span>core/<span class="va">jasmine</span>.<span class="at">js</span><span class="op">:</span><span class="dv">1564</span><span class="op">:</span><span class="dv">12</span>)
at <span class="va">Object</span>.<span class="op">&lt;</span>anonymous<span class="op">&gt;</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/spec/<span class="va">TurnList</span>.<span class="at">js</span><span class="op">:</span><span class="dv">39</span><span class="op">:</span><span class="dv">36</span>)
at <span class="at">attemptSync</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/node_modules/jasmine<span class="op">-</span>core/lib/jasmine<span class="op">-</span>core/<span class="va">jasmine</span>.<span class="at">js</span><span class="op">:</span><span class="dv">1950</span><span class="op">:</span><span class="dv">24</span>)
at <span class="va">QueueRunner</span>.<span class="at">run</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/node_modules/jasmine<span class="op">-</span>core/lib/jasmine<span class="op">-</span>core/<span class="va">jasmine</span>.<span class="at">js</span><span class="op">:</span><span class="dv">1938</span><span class="op">:</span><span class="dv">9</span>)
at <span class="va">QueueRunner</span>.<span class="at">execute</span> (<span class="ss">/Users/salva/workspace/pvli2017</span><span class="op">-</span>rpg<span class="op">-</span>battle/node_modules/jasmine<span class="op">-</span>core/lib/jasmine<span class="op">-</span>core/<span class="va">jasmine</span>.<span class="at">js</span><span class="op">:</span><span class="dv">1923</span><span class="op">:</span><span class="dv">10</span>)</code></pre></div>
<p>La traza contiene el fallo y dónde se ha producido en el conjunto de llamadas desde la más reciente hasta la más vieja. A veces los fallos son producto de implementaciones que no cumplen las expectativas, otras veces serán fallos en tiempo de ejecución y otras serán fallos de sintaxis.</p>
<p>Acostúmbrate a fallar y a encontrar en la traza el punto exacto del código que está bajo tu control para solucionarlo. Para ello busca las carpetas <code>spec</code> y <code>src</code> entre la traza. El primer número tras la ruta es la línea del fallo.</p>
<h3 id="activando-y-desactivando-tests">Activando y desactivando tests</h3>
<p>Los tests y las suites pueden desactivarse añadiendo el prefijo <code>x</code>. Por ejemplo:</p>
<div class="sourceCode"><pre class="sourceCode js"><code class="sourceCode javascript"><span class="at">describe</span>(<span class="st">&#39;Los suites en Jasmine&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>

<span class="at">describe</span>(<span class="st">&#39;pueden anidarse&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>

<span class="at">it</span>(<span class="st">&#39;y encierran tests con expectativas&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>
<span class="at">expect</span>(<span class="dv">2</span> <span class="op">+</span> <span class="dv">2</span>).<span class="at">toBe</span>(<span class="dv">4</span>)<span class="op">;</span>
<span class="op">}</span>)<span class="op">;</span>

<span class="at">xit</span>(<span class="st">&#39;este test está desactivado&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>

<span class="op">}</span>)<span class="op">;</span>

<span class="op">}</span>)<span class="op">;</span>

<span class="at">xdescribe</span>(<span class="st">&#39;la suite y todos sus tests están desactivados.&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>

<span class="op">}</span>)<span class="op">;</span>

<span class="op">}</span>)<span class="op">;</span></code></pre></div>
<p>Los tests desactivados no comprueban las expectativas pero Jasmine nos informará de ellos.</p>
<p>Recuerda que la práctica sólo puede aprobarse si no hay test fallando ni pendientes.</p>
<h3 id="el-ciclo-de-desarrollo">El ciclo de desarrollo</h3>
<p>Cuando estés desarrollando, es conveniente que pases los test a menudo por dos motivos: + Comprobar que avanzas. + Comprobar que no has roto nada.</p>
<p>Para ello puedes ejecutar el comando:</p>
<pre><code>$ npm run-script watch</code></pre>
<p>Esta tarea monitoriza los cambios en los archivos de las carpetas <code>spec</code> y <code>src</code> y cuando detecte un cambio, lanzara todos los tests.</p>
<p>A veces, el error es tan estrepitoso que rompe la monitorización. En tal caso tendrás que reintroducir el comando manualmente.</p>
<h3 id="depurando-tests-asíncronos">Depurando tests asíncronos</h3>
<p>Algunos tests son asíncronos y pueden producir <em>timeouts</em>. En general un <em>timeout</em> no es un resultado positivo. El problema de los <em>timeouts</em> es que pueden ralentizar toda la suite así que la recomendación en estos casos es afrontarlos uno a uno, desactivando el resto y activándolos poco a poco.</p>
<p>Reconocerás un test asíncrono porque lleva un parámetros <code>done</code> como en el ejemplo:</p>
<div class="sourceCode"><pre class="sourceCode js"><code class="sourceCode javascript"><span class="kw">var</span> EventEmitter <span class="op">=</span> <span class="at">require</span>(<span class="st">&#39;events&#39;</span>).<span class="at">EventEmitter</span><span class="op">;</span>

<span class="at">describe</span>(<span class="st">&#39;EventEmitter&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>

<span class="at">it</span>(<span class="st">&#39;emite eventos arbitrarios&#39;</span><span class="op">,</span> <span class="kw">function</span> (done) <span class="op">{</span>
<span class="kw">var</span> ee <span class="op">=</span> <span class="kw">new</span> <span class="at">EventEmitter</span>()<span class="op">;</span>
<span class="va">ee</span>.<span class="at">on</span>(<span class="st">&#39;turn&#39;</span><span class="op">,</span> <span class="kw">function</span>(turn) <span class="op">{</span>
<span class="at">expect</span>(<span class="va">turn</span>.<span class="at">number</span>).<span class="at">toBe</span>(<span class="dv">1</span>)<span class="op">;</span>
<span class="at">done</span>()<span class="op">;</span>
<span class="op">}</span>)<span class="op">;</span>
<span class="va">ee</span>.<span class="at">emit</span>(<span class="st">&#39;turn&#39;</span><span class="op">,</span> <span class="op">{</span> <span class="dt">number</span><span class="op">:</span> <span class="dv">1</span> <span class="op">}</span>)<span class="op">;</span>
<span class="op">}</span>)<span class="op">;</span>

<span class="op">}</span>)<span class="op">;</span></code></pre></div>
<h2 id="estrategia-general-para-la-depuración">Estrategia general para la depuración</h2>
<p>Es muy recomendable que mantengas una rama estable donde todos los tests pasen y los que no, estén desactivados. Cuando te embarques en la tarea de hacer que un test pase, crea una rama para esa tarea y cuando termines mézclala con la rama estable.</p>
<p>Cuando encuentres un error, intenta seguir los siguientes pasos: 1. Desactiva los tests asíncronos que estén tardando rápido. <strong>Necesitas un ciclo de desarrollo rápido.</strong> 2. <strong>¡¡Lee el error!!</strong>. 3. Busca en la traza el lugar donde se original el error: 1. Si es un fallo en una expectativa, localiza el punto de entrada en tu código. 2. Deja trazas con <code>console.log()</code> inspeccionando el estado de tus objetos. 4. Salva y relanza los tests a menudo.</p>
</body>
</html>
@@ -0,0 +1,58 @@
# Criterios de evaluación

La práctica sea realizará por grupos, y se hará una sola entrega por cada grupo
siguiendo las [instrucciones comunes][] de entrega para la asignatura. Se
aplicarán los criterios usuales en cuanto a entregas, obligatoriedad y copias
que rigen la evaluación de la asignatura.

**Se recuerda se hará en control exhaustivo de las copias y que copiar implica
suspender la convocatoria actual**.

# Realización

Uno de los objetivos principales de este tipo de actividades es que los alumnos
sean capaces de ser autónomos, evaluar su propia entrega y ofrecer soluciones
originales y personales. Es decir, hay hueco para improvisar, añadir elementos
o proponer modificaciones razonables (siempre consensuadas y validadas a priori
con el equipo docente).

Además, es fundamental que los grupos trabajen de forma autónoma, con tiempo de
sobra y consultando al equipo docente. La idea es que, al hacerlo así, tener un
10 en la práctica sea razonablemente fácil.

# Puntuación

**Si algún *test* no se pasa, la práctica estará suspensa**. Para evitar este
caso, recomendamos que se empiece la práctica lo antes posible y que se haga
uso de los foros y tutorías con el equipo docente.

Pasar el 100% de los *tests* correctamente (es decir, habiendo implementado de
manera correcta, en JavaScript, cada una de las partes) supone un 5 en la
práctica. Si se pasan todos los *tests* pero algún aspecto no está bien
implementado, puede que la nota sea menor.

A partir del punto en el que todos los *test* se cumplan, se otorgarán las
siguientes puntuaciones. La nota indicada representa el máximo al que se
aspira en cada apartado. Este máximo se conseguirá teniendo en cuenta la
corrección del código, la elegancia, la eficiencia y la coherencia con el
enunciado y con el resto de la solución particular.

## Puntuación adicional

- Usa las funciones para recorrer listas como `forEach()`, `map()` o `filter()`
en lugar de un bucle: **1 punto**.
- Utiliza funciones auxiliares para simplificar el código: **1 punto**.
- En `src/Battle.js`, en el método `_onAction()`, evitar las construcciones
`if` y `switch` para llamar al método correspondiente a la acción: **1 punto**.
- Utiliza un objeto para llevar el tracking de las defensas originales:
**1 punto**.
- Pasar el _linter_: **1 punto**.

# Fecha y modo de entrega

La fecha de entrega está reflejada en la actividad de entrega del Campus
Virtual.

Se entregará la práctica según las [instrucciones comunes][] de la asignatura.

[instrucciones comunes]: https://clnznr.github.io/pvli2017/website/general/criterios_evaluacion.html
@@ -0,0 +1,23 @@
body {
font-family: georgia;
margin-bottom: 2em;
margin-top: 1em;
margin-left: 2em;
margin-right: 2em;
text-align: justify;
/* width: 35em; */
alignment-baseline: central;
}

h1.title {
font-size: 3em;
text-align: center;
}

h2.author {
text-align: center;
}

h3.date {
text-align: center;
}
@@ -0,0 +1,22 @@
var gulp = require('gulp');
var jasmine = require('gulp-jasmine');
var eslint = require('gulp-eslint');

gulp.task('watch', ['test'], function () {
gulp.watch('./src/**/*', ['test']);
gulp.watch('./spec/**/*', ['test']);
});

gulp.task('lint', function () {
gulp.src('./src/**/*')
.pipe(eslint())
.pipe(eslint.format());
});

gulp.task('test', ['lint'], function () {
gulp.src('./spec/**/*')
.pipe(jasmine({ includeStackTrace: true }));
});


gulp.task('default', ['test']);
@@ -0,0 +1,268 @@
var readline = require('readline');

var Battle = require('./src/Battle');

var entities = require('./src/entities');

var cmd = readline.createInterface({
input: process.stdin,
outpu: process.stdout,
prompt: '> '
});

var parties;
var partyNames = { heroes: 'héroes', monsters: 'monstruos' };
var action;
var battle;
var battleLine;

setupBattle();

function setupBattle() {
battle = new Battle();
battle.setup(getRandomSetup());

battle.on('start', function (charactersByParties) {
parties = charactersByParties;
console.log('¡La batalla comienza!');
});

battle.on('end', function (result) {
console.log('¡Fin de la batalla!');
console.log('Bando ganador: ' + result.winner);
process.exit(0);
});

battle.on('turn', function (turn) {
console.log(
'Turno', turn.number + '.'
);
showTurnLine(turn.activeCharacterId);
showActions(this.options);
});

battle.on('info', function (result) {
switch (result.action) {
case 'attack':
if (!result.success) {
console.log(
result.activeCharacterId,
'trató de golpear a', result.targetId,
'y falló.'
);
} else {
console.log(
result.activeCharacterId,
'golpea a', result.targetId,
'y le produce', result.effect
);
}
break;
case 'defend':
console.log(
result.targetId,
'defendió. Su defensa es ahora',
result.newDefense
);
break;
case 'cast':
if (!result.success) {
console.log(
result.activeCharacterId,
'trató de lanzar un hechizo a', result.targetId,
'y falló.'
);
} else {
console.log(
result.activeCharacterId,
'lanza', result.scrollName, 'a', result.targetId,
'con efecto', result.effect
);
}
break;
}
console.log();
});

battle.start();
}

function showActions() {
var actions = {
attack: 'Atacar',
defend: 'Defender',
cast: 'Lanzar hechizo'
};
var items = battle.options.list();

console.log('Elige qué hacer:');
items.forEach(function (item, index) {
console.log('[' + (index + 1) + ']', actions[item]);
});
waitForAction();

function waitForAction() {
cmd.prompt();
cmd.once('line', function (selection) {
selection = parseInt(selection);
if (!isNaN(selection) && selection > 0 && selection <= items.length) {
var id = items[selection - 1];
battle.options.select(items[selection - 1]);
action = id;
if (id === 'attack') {
showTargets();
}
if (id === 'cast') {
showScrolls();
}
} else {
console.log('Opción incorrecta');
setTimeout(function () {
readline.moveCursor(process.stdin, 0, -2);
readline.clearScreenDown(process.stdin);
waitForAction();
}, 500);
}
});
}
}

function showTargets() {
var items = battle.options.list();
console.log('Elige un objetivo:');
items.forEach(function (item, index) {
console.log('[' + (index + 1) + ']', item);
});
console.log('\n[0] Cancelar');
waitForAction();

function waitForAction() {
cmd.prompt();
cmd.once('line', function (selection) {
selection = parseInt(selection);
if (!isNaN(selection) && selection >= 0 && selection <= items.length) {
if (selection === 0) {
battle.options.cancel();
readline.moveCursor(process.stdin, 0, -(4 + items.length));
readline.clearScreenDown(process.stdin);
if (action === 'attack') {
showActions();
}
if (action === 'cast') {
showScrolls();
}
} else {
var id = items[selection - 1];
battle.options.select(items[selection - 1]);
}
} else {
console.log('Opción incorrecta');
setTimeout(function () {
readline.moveCursor(process.stdin, 0, -2);
readline.clearScreenDown(process.stdin);
waitForAction();
}, 500);
}
});
}
}

function showScrolls() {
var items = battle.options.list();
console.log('Elige un hechizo:');
items.forEach(function (item, index) {
console.log(
'[' + (index + 1) + ']', item,
'(' + battle.options.get(item).cost + ' MP)'
);
});
console.log('\n[0] Cancelar');
waitForAction();

function waitForAction() {
cmd.prompt();
cmd.once('line', function (selection) {
selection = parseInt(selection);
if (!isNaN(selection) && selection >= 0 && selection <= items.length) {
if (selection === 0) {
battle.options.cancel();
readline.moveCursor(process.stdin, 0, -(4 + items.length));
readline.clearScreenDown(process.stdin);
showActions();
} else {
var id = items[selection - 1];
battle.options.select(items[selection - 1]);
readline.moveCursor(process.stdin, 0, -(4 + items.length));
readline.clearScreenDown(process.stdin);
showTargets();
}
} else {
console.log('Opción incorrecta');
setTimeout(function () {
readline.moveCursor(process.stdin, 0, -2);
readline.clearScreenDown(process.stdin);
waitForAction();
}, 500);
}
});
}
}

function showTurnLine(activeCharacterId) {
var nameRegExp = new RegExp(activeCharacterId);
Object.keys(parties).forEach(function (partyId) {
console.log('Bando de los', partyNames[partyId] + ':');
var characters = parties[partyId];
characters.forEach(function (charId) {
var character = battle.characters.get(charId);
var hp = character.hp;
var maxHp = character.maxHp;
var mp = character.mp;
var maxMp = character.maxMp;
var deadToken = character.hp === 0 ? '✝' : ' ';
console.log(
charId === activeCharacterId ? '>' : deadToken,
charId,
hp + '/' + maxHp + ' HP',
mp + '/' + maxMp + ' MP'
);
});
});
console.log();
}

function getRandomSetup() {
var heroMembers = [
entities.characters.heroTank,
entities.characters.heroWizard
];
var monsterMembers = getMonsterParty();
return {
heroes: {
members: heroMembers,
grimoire: [entities.scrolls.health, entities.scrolls.fireball]
},
monsters: {
members: monsterMembers
}
};
}

function getMonsterParty() {
var partySize = Math.floor(Math.random() * 3) + 1;
var members = [];
for (var i = 0; i < partySize; i++) {
members.push(getRandomMonster());
}
return members;
}

function getRandomMonster() {
var monsters = Object.keys(entities.characters).filter(isMonster);
var randomId = monsters[Math.floor(Math.random() * monsters.length)];
return entities.characters[randomId];

function isMonster(id) {
return id.substr(0, 'monster'.length) === 'monster';
}
}
@@ -0,0 +1,18 @@
{
"name": "pvli2017-rpg-battle",
"version": "1.0.0",
"description": "Skeleton for practise 0 of PVLI course 2017",
"main": "index.js",
"scripts": {
"test": "node ./node_modules/gulp/bin/gulp.js test",
"watch": "node ./node_modules/gulp/bin/gulp.js watch"
},
"author": "Salvador de la Puente González <salva@mozilla.com>",
"license": "ISC",
"devDependencies": {
"gulp": "^3.9.1",
"gulp-eslint": "^3.0.1",
"gulp-jasmine": "^2.4.2",
"mockery": "^2.0.0"
}
}

Large diffs are not rendered by default.

@@ -0,0 +1,93 @@
var samples = require('./samplelib');

describe('CharactesView type', function () {
'use strict';

var CharactersView = require('../src/CharactersView');

var charactersView;

var heroTank = samples.characters.heroTank;
var heroWizard = samples.characters.heroWizard;

var visibleFeatures = [
'name',
'party',
'initiative',
'defense',
'hp',
'mp',
'maxHp',
'maxMp'
];

beforeAll(function () {
heroTank.party = 'teamA';
heroWizard.party = 'teamB';
});

beforeEach(function () {
charactersView = new CharactersView();
charactersView.set({
Tank: heroTank,
Wizz: heroWizard
});
});

it('shows only the visible features and includes id.', function () {
var heroTankView = charactersView.get('Tank');
var featuresCount = Object.keys(heroTankView).length ;

expect(featuresCount).toBe(visibleFeatures.length);
visibleFeatures.forEach(function (feature) {
expect(heroTankView[feature]).toEqual(heroTank[feature]);
});
});

it('list all characters.', function () {
var heroTankView = charactersView.get('Tank');
var heroWizardView = charactersView.get('Wizz');
expect(charactersView.all())
.toEqual({
Tank: heroTankView,
Wizz: heroWizardView
});
});

it('list all characters by party', function () {
var heroTankView = charactersView.get('Tank');
var heroWizardView = charactersView.get('Wizz');
expect(charactersView.allFrom('teamA'))
.toEqual({
Tank: heroTankView
});
expect(charactersView.allFrom('teamB'))
.toEqual({
Wizz: heroWizardView
});
});

it('does not allow to modify character\'s features', function () {
var heroTankView = charactersView.get('Tank');

visibleFeatures.forEach(function (feature) {
var expectedValue = heroTankView[feature];
heroTankView[feature] = expectedValue + 1;
expect(heroTankView[feature]).toEqual(expectedValue);
});
});

it('does not modify the original character.', function () {
var heroTankView = charactersView.get('Tank');

var expectedValues = visibleFeatures.reduce(function (values, feature) {
values[feature] = heroTank[feature];
return values;
}, {});

visibleFeatures.forEach(function (feature) {
heroTankView[feature] += 1;
expect(heroTank[feature]).toEqual(expectedValues[feature]);
});
});
});
@@ -0,0 +1,57 @@
describe('Options type', function () {
'use strict';

var Options = require('../src/Options');

var options;

var dataFor = {
itemA: {},
itemB: {},
itemC: {}
};

var items = {
itemA: dataFor.itemA,
itemB: dataFor.itemB,
itemC: dataFor.itemC
};

beforeEach(function () {
options = new Options(items);
});

it('allows the empty menu.', function () {
options = new Options();
expect(options.list()).toEqual([]);
});

it('list all the options.', function () {
expect(options.list())
.toEqual(jasmine.arrayContaining(Object.keys(items)));
});

it('recovers data associated to the menu item.', function () {
expect(options.get('itemA')).toBe(dataFor['itemA']);
});

it('emits an event when selecting an entry.', function (done) {
var entryId = 'itemA';
options.on('chose', function (id, data) {
expect(id).toBe(entryId);
expect(data).toBe(dataFor[entryId]);
done();
});
options.select(entryId);
});

it('emits an error event when the entry does not exist.', function (done) {
var entryId = 'xxxx';
options.on('choseError', function (reason, id) {
expect(reason).toBe('option-does-not-exist');
expect(id).toBe(entryId);
done();
});
options.select(entryId);
});
});
@@ -0,0 +1,80 @@
'use strict';

var mockery = require('mockery');

describe('OptionsStack type', function () {
var OptionsStack;
var optionsStack;

var MockOptions = jasmine.createSpy('MockOptions');
MockOptions.prototype.select = function() {};
MockOptions.prototype.list = function() {};
MockOptions.prototype.get = function() {};

beforeAll(function () {
mockery.registerMock('./Options', MockOptions);
mockery.enable({
useCleanCache: true,
warnOnUnregistered: false
});

OptionsStack = require('../src/OptionsStack');
});

afterAll(function () {
mockery.disable();
mockery.deregisterMock('./Options');
});

beforeEach(function () {
spyOn(MockOptions.prototype, 'select');
spyOn(MockOptions.prototype, 'list');
spyOn(MockOptions.prototype, 'get');
optionsStack = new OptionsStack();
});

it('adds an options group by assigning a group to current.', function () {
var group = new MockOptions();
optionsStack.current = group;
expect(optionsStack.current).toBe(group);
});

it('adds an options group by assigning a object to current.', function () {
var group = { a: 1, b: 2};
optionsStack.current = group;
expect(MockOptions).toHaveBeenCalledWith(group);
expect(optionsStack.current).toEqual(jasmine.any(MockOptions));
});

it('returns to the previous options group when calling cancel().',
function () {
var group = new MockOptions();
var group2 = new MockOptions();
optionsStack.current = group;
optionsStack.current = group2;
expect(optionsStack.current).toBe(group2);
optionsStack.cancel();
expect(optionsStack.current).toBe(group);
});

it('proxies get() to the latest options group.', function () {
var group = new MockOptions();
optionsStack.current = group;
optionsStack.get();
expect(MockOptions.prototype.get).toHaveBeenCalled();
});

it('proxies select() to the latest options group.', function () {
var group = new MockOptions();
optionsStack.current = group;
optionsStack.select('x');
expect(MockOptions.prototype.select).toHaveBeenCalledWith('x');
});

it('proxies list() to the latest options group.', function () {
var group = new MockOptions();
optionsStack.current = group;
optionsStack.list();
expect(MockOptions.prototype.list).toHaveBeenCalled();
});
});
@@ -0,0 +1,71 @@
'use strict';

describe('The TurnList type', function () {
var TurnList = require('../src/TurnList');
var turnList;
var characters;

function FakeCharacter(party, inititative, isDead) {
this.party = party;
this.initiative = inititative;
this._isDead = isDead;
}
FakeCharacter.prototype.isDead = function () {
return this._isDead;
};

beforeEach(function () {
characters = {
a: new FakeCharacter('heroes', 1),
b: new FakeCharacter('heroes', 5),
c: new FakeCharacter('monsters', 10)
};

turnList = new TurnList();
turnList.reset(characters);
});

it('accepts a set of characters and sort them by inititative.', function () {
expect(turnList.turnNumber).toBe(0);
expect(turnList.activeCharacterId).toBe(null);
expect(turnList.list).toEqual(['c', 'b', 'a']);
});

it('accepts a set of characters and sort them by inititative.', function () {
var turn = turnList.next();

expect(turn.number).toBe(1);
expect(turn.party).toBe(characters.c.party);
expect(turn.activeCharacterId).toBe('c');

expect(turnList.turnNumber).toBe(1);
expect(turnList.activeCharacterId).toBe('c');
});

it('ignore all dead characters', function () {
characters.c._isDead = true;
characters.b._isDead = true;
var turn = turnList.next();

expect(turn.number).toBe(1);
expect(turn.party).toBe(characters.a.party);
expect(turn.activeCharacterId).toBe('a');

expect(turnList.turnNumber).toBe(1);
expect(turnList.activeCharacterId).toBe('a');
});

it('starts over when reaching the end of the list.', function () {
turnList.next();
turnList.next();
turnList.next();
var turn = turnList.next();

expect(turn.number).toBe(4);
expect(turn.party).toBe(characters.c.party);
expect(turn.activeCharacterId).toBe('c');

expect(turnList.turnNumber).toBe(4);
expect(turnList.activeCharacterId).toBe('c');
});
});

Large diffs are not rendered by default.

@@ -0,0 +1,61 @@
var entities = require('../src/entities');

var Character = entities.Character;
var Weapon = entities.Weapon;
var Scroll = entities.Scroll;
var Effect = entities.Effect;

var lib = module.exports = {
weapons: {
get sword() { return new Weapon('Iron sword', 25); },
get wand() { return new Weapon('Wood wand', 5, { mp: -5 }); },
get claws() { return new Weapon('Claws', 15); }
},

characters: {
get heroTank() {
return new Character('Tank', {
initiative: 10,
weapon: lib.weapons.sword,
defense: 70,
hp: 80,
maxHp: 80,
mp: 0,
maxMp: 0
});
},

get heroWizard() {
return new Character('Wizz', {
initiative: 4,
weapon: lib.weapons.wand,
defense: 50,
hp: 40,
maxHp: 40,
mp: 100,
maxMp: 100
});
},

get fastEnemy() {
return new Character('Fasty', {
initiative: 30,
weapon: lib.weapons.claws,
defense: 40,
hp: 30,
maxHp: 30,
mp: 100,
maxMp: 100
});
}
},

scrolls: {
get health() {
return new Scroll('Health', 10, new Effect({ hp: 25 }));
},
get fire() {
return new Scroll('Fire', 30, new Effect({ hp: -25 }));
}
}
};
@@ -0,0 +1,35 @@
'use strict';

describe('Utils module', function () {
var utils = require('../src/utils');

it('has a listToMap() to convert from a list to an object choosing the key. ',
function () {
var list = [
{ name: 'a', hp: 1 },
{ name: 'b', hp: 2 },
{ name: 'c', hp: 3 }
];
expect(utils.listToMap(list, useName)).toEqual({
a: { name: 'a', hp: 1 },
b: { name: 'b', hp: 2 },
c: { name: 'c', hp: 3 }
});

function useName(item) { return item.name; }
});

it('has mapValues() returning a list of the values of a map', function () {
var map = {
a: { name: 'a', hp: 1 },
b: { name: 'b', hp: 2 },
c: { name: 'c', hp: 3 }
};
expect(utils.mapValues(map)).toEqual([
{ name: 'a', hp: 1 },
{ name: 'b', hp: 2 },
{ name: 'c', hp: 3 }
]);
});

});
@@ -0,0 +1,295 @@
'use strict';

var EventEmitter = require('events').EventEmitter;
var CharactersView = require('./CharactersView');
var OptionsStack = require('./OptionsStack');
var TurnList = require('./TurnList');
var Effect = require('./items').Effect;

var utils = require('./utils');
var listToMap = utils.listToMap;
var mapValues = utils.mapValues;

function Battle() {
EventEmitter.call(this);
this._grimoires = {};
this._charactersById = {};
this._turns = new TurnList();

this.options = new OptionsStack();
this.characters = new CharactersView();
}
Battle.prototype = Object.create(EventEmitter.prototype);
Battle.prototype.constructor = Battle;

Object.defineProperty(Battle.prototype, 'turnList', {
get: function () {
return this._turns ? this._turns.list : null;
}
});

Battle.prototype.setup = function (parties) {
this._grimoires = this._extractGrimoiresByParty(parties);
this._charactersById = this._extractCharactersById(parties);
this._states = this._resetStates(this._charactersById);

this._turns.reset(this._charactersById);

this.characters.set(this._charactersById);
this.options.clear();
};

Battle.prototype.start = function () {
this._inProgressAction = null;
this._stopped = false;
this.emit('start', this._getCharIdsByParty());
this._nextTurn();
};

Battle.prototype.stop = function () {
this._stopped = true;
};

Object.defineProperty(Battle.prototype, '_activeCharacter', {
get: function () {
return this._charactersById[this._turns.activeCharacterId];
}
});

Battle.prototype._extractGrimoiresByParty = function (parties) {
var grimoires = {};
var partyIds = Object.keys(parties);
partyIds.forEach(function (partyId) {
var partyGrimoire = parties[partyId].grimoire || [];
grimoires[partyId] = listToMap(partyGrimoire, useName);
});
return grimoires;

function useName(scroll) {
return scroll.name;
}
};

Battle.prototype._extractCharactersById = function (parties) {
var idCounters = {};
var characters = [];
var partyIds = Object.keys(parties);
partyIds.forEach(function (partyId) {
var members = parties[partyId].members;
assignParty(members, partyId);
characters = characters.concat(members);
});
return listToMap(characters, useUniqueName);

function assignParty(characters, party) {
// Cambia la party de todos los personajes a la pasada como parámetro.
characters.forEach(function (character){
character.party = party;
});
}

function useUniqueName(character) {
// Genera nombres únicos de acuerdo a las reglas
// de generación de identificadores que encontrarás en
// la descripción de la práctica o en la especificación.
var nombre = character.name;
if (idCounters[nombre] !== undefined){
idCounters[nombre] ++;
return nombre + ' ' + idCounters[nombre];
}
else{
idCounters[nombre] = 0;
idCounters[nombre] ++;
return nombre;
}

}
};

Battle.prototype._resetStates = function (charactersById) {
return Object.keys(charactersById).reduce(function (map, charId) {
map[charId] = {};
return map;
}, {});
};

Battle.prototype._getCharIdsByParty = function () {
var charIdsByParty = {};
var charactersById = this._charactersById;
Object.keys(charactersById).forEach(function (charId) {
var party = charactersById[charId].party;
if (!charIdsByParty[party]) {
charIdsByParty[party] = [];
}
charIdsByParty[party].push(charId);
});
return charIdsByParty;
};

Battle.prototype._nextTurn = function () {
if (this._stopped) { return; }
setTimeout(function () {
var endOfBattle = this._checkEndOfBattle();
if (endOfBattle) {
this.emit('end', endOfBattle);
} else {
var turn = this._turns.next();
this._showActions();
this.emit('turn', turn);
}
}.bind(this), 0);
};

Battle.prototype._checkEndOfBattle = function () {
var allCharacters = mapValues(this._charactersById);
var aliveCharacters = allCharacters.filter(isAlive);
var commonParty = getCommonParty(aliveCharacters);
return commonParty ? { winner: commonParty } : null;

function isAlive(character) {
// Devuelve true si el personaje está vivo.
return !character.isDead();
}

function getCommonParty(characters) {
// Devuelve la party que todos los personajes tienen en común o null en caso
// de que no haya común.
var auxp = characters[0].party;
var encontrado = false;

characters.forEach(function (character){
if (auxp !== character.party)
encontrado = true;
});

return encontrado ? null : auxp;

}
}

Battle.prototype._showActions = function () {
this.options.current = {
'attack': true,
'defend': true,
'cast': true
};
this.options.current.on('chose', this._onAction.bind(this));
};

Battle.prototype._onAction = function (action) {
this._action = {
action: action,
activeCharacterId: this._turns.activeCharacterId
};
// Debe llamar al método para la acción correspondiente:
// defend -> _defend; attack -> _attack; cast -> _cast
if (action === 'defend')
this.emit(this._action, this._defend());
else if (action === 'attack')
this.emit(this._action, this._attack());
else if (action === 'cast')
this.emit(this._action, this._cast());

};

Battle.prototype._defend = function () {
var activeCharacterId = this._action.activeCharacterId;
var newDefense = this._improveDefense(activeCharacterId);
this._action.targetId = this._action.activeCharacterId;
this._action.newDefense = newDefense;
this._executeAction();
};

Battle.prototype._improveDefense = function (targetId) {
var states = this._states[targetId];
if (!states.defense)
states.defense = this._charactersById[targetId].defense || 0;

var ndef = Math.ceil(this._charactersById[targetId].defense * 1.1);
this._charactersById[targetId].defense = ndef;
return ndef;
// Implementa la mejora de la defensa del personaje.
};

Battle.prototype._restoreDefense = function (targetId) {
// Restaura la defensa del personaje a cómo estaba antes de mejorarla.
// Puedes utilizar el atributo this._states[targetId] para llevar tracking
// de las defensas originales.
// console.log('///////////////////'+this._states[targetId].defense);
this._charactersById[targetId].defense = this._states[targetId].defense || 0;
};

Battle.prototype._attack = function () {
var self = this;
self._showTargets(function onTarget(targetId) {
// Implementa lo que pasa cuando se ha seleccionado el objetivo.
self._action.effect = self._charactersById[self._action.activeCharacterId].weapon.extraEffect;
self._action.targetId = targetId;
self._executeAction();
self._restoreDefense(targetId);
});
};

Battle.prototype._cast = function () {
var self = this;
self._showScrolls(function onScroll(scrollId, scroll) {
// Implementa lo que pasa cuando se ha seleccionado el hechizo.
self._showTargets(function onTarget(targetId){
self._action.targetId = targetId;
self._action.effect = scroll.effect;
self._action.scrollName = scrollId;
self._charactersById[self._action.activeCharacterId].mp -= scroll.cost;
self._executeAction();
self._restoreDefense(targetId);
});

});
};

Battle.prototype._executeAction = function () {
var action = this._action;
var effect = this._action.effect || new Effect({});
var activeCharacter = this._charactersById[action.activeCharacterId];
var targetCharacter = this._charactersById[action.targetId];
var areAllies = activeCharacter.party === targetCharacter.party;

var wasSuccessful = targetCharacter.applyEffect(effect, areAllies);
this._action.success = wasSuccessful;

this._informAction();
this._nextTurn();
};

Battle.prototype._informAction = function () {
this.emit('info', this._action);
};

Battle.prototype._showTargets = function (onSelection) {
// Toma ejemplo de la función ._showActions() para mostrar los identificadores
// de los objetivos.
var enemigos = {};
for ( var name in this._charactersById){
if(!this._charactersById[name].isDead()){
enemigos[name] = name;
}
}
this.options.current = enemigos;
this.options.current.on('chose', onSelection);
};

Battle.prototype._showScrolls = function (onSelection) {
// Toma ejemplo de la función anterior para mostrar los hechizos. Estudia
// bien qué parámetros se envían a los listener del evento chose.
var acor = this._charactersById[this._action.activeCharacterId];
var elem = {};

for (var num in this._grimoires[acor.party]){
if (this._grimoires[acor.party][num].canBeUsed(acor.mp))
elem[num] = this._grimoires[acor.party][num];
}

this.options.current = elem;
this.options.current.on('chose', onSelection);
};

module.exports = Battle;
@@ -0,0 +1,72 @@
'use strict';
var dice = require('./dice');

function Character(name, features) {
features = features || {};
this.name = name;
this.party = features.party || null;
this.initiative = features.initiative || 0;
this._defense = features.defense || 0;
this.weapon = features.weapon || null;
this._mp = features.mp || 0;
this._hp = features.hp || 0;
// Extrae del parámetro features cada característica y alamacénala en
// una propiedad.
this.maxMp = features.maxMp || this._mp || 0;
this.maxHp = features.maxHp || this._hp || 15;
}

Character.prototype._immuneToEffect = ['name', 'weapon'];

Character.prototype.isDead = function () {
// Rellena el cuerpo de esta función
return !(this.hp > 0);

};

Character.prototype.applyEffect = function (effect, isAlly) {
// Implementa las reglas de aplicación de efecto para modificar las
// características del personaje. Recuerda devolver true o false según
// si el efecto se ha aplicado o no.
var success = true;

if (!isAlly) {
success = dice.d100() > this._defense;
}
if (success){
for (var name in effect)
this[name] += effect[name];
}
return success;
};

Object.defineProperty(Character.prototype, 'mp', {
get: function () {
return this._mp;
},
set: function (newValue) {
this._mp = Math.max(0, Math.min(newValue, this.maxMp));
}
});

Object.defineProperty(Character.prototype, 'hp', {
// Puedes usar la mísma ténica que antes para mantener el valor de hp en el
// rango correcto.
get: function () {
return this._hp;
},
set: function (newValue) {
this._hp = Math.max(0, Math.min(newValue, this.maxHp));
}
});
Object.defineProperty(Character.prototype, 'defense', {
// Puedes hacer algo similar a lo anterior para mantener la defensa entre 0 y
// 100.
get: function () {
return this._defense;
},
set: function (newValue) {
this._defense = Math.max(0, Math.min(newValue, 100));
}
});
module.exports = Character;
@@ -0,0 +1,67 @@
'use strict';

function CharactersView() {
this._views = {};
}

CharactersView.prototype._visibleFeatures = [
'name',
'party',
'initiative',
'defense',
'hp',
'mp',
'maxHp',
'maxMp'
];

CharactersView.prototype.all = function () {
return Object.keys(this._views).reduce(function (copy, id) {
copy[id] = this._views[id];
return copy;
}.bind(this), {});
};

CharactersView.prototype.allFrom = function (party) {
return Object.keys(this._views).reduce(function (copy, id) {
if (this._views[id].party === party) {
copy[id] = this._views[id];
}
return copy;
}.bind(this), {});
};

CharactersView.prototype.get = function (id) {
return this._views[id] || null;
};

CharactersView.prototype.set = function (characters) {
this._views = Object.keys(characters).reduce(function (views, id) {
views[id] = this._getViewFor(characters[id]);
return views;
}.bind(this), {});
};

CharactersView.prototype._getViewFor = function (character) {
var view = {};
// Usa la lista de características visibles y Object.defineProperty() para
// devolver un objeto de JavaScript con las características visibles pero
// no modificables.
this._visibleFeatures.forEach(function(esp){
Object.defineProperty(view, esp, {
get: function () {
// ¿Cómo sería este getter para reflejar la propiedad del personaje?
return character[esp];
},
set: function (value) {
// ¿Y este setter para ignorar cualquier acción?
return character[esp] + value * 0;
},
enumerable: true
});

});
return view;
};

module.exports = CharactersView;
@@ -0,0 +1,36 @@
'use strict';

var EventEmitter = require('events').EventEmitter;

function Options(group) {
EventEmitter.call(this);
this._group = typeof group === 'object' ? group : {};
}
Options.prototype = Object.create(EventEmitter.prototype);
Options.prototype.constructor = Options;

Options.prototype.list = function () {
return Object.keys(this._group);
};

Options.prototype.get = function (id) {
return this._group[id];
};

Options.prototype.select = function (id) {
// Haz que se emita un evento cuando seleccionamos una opción.
var list2 = this.list();
var i = 0;
var centinela = true;
while(i < list2.length && centinela){
if (list2[i] === id){
centinela = false;
this.emit('chose', id, this.get(id));
}
i++;
}
if ( i >= list2.length)
this.emit('choseError', 'option-does-not-exist', id);
};

module.exports = Options;
@@ -0,0 +1,41 @@
'use strict';
var Options = require('./Options');

function OptionsStack() {
this._stack = [];
Object.defineProperty(this, 'current', {
get: function () {
return this._stack[this._stack.length - 1];
},
set: function (v) {
if (!(v instanceof Options)) {
v = new Options(v);
}
return this._stack.push(v);
}
});
}

OptionsStack.prototype.select = function (id) {
// Redirige el comando al último de la pila.
return this.current.select(id);
};

OptionsStack.prototype.list = function () {
// Redirige el comando al último de la pila.
return this.current.list();
};

OptionsStack.prototype.get = function (id) {
return this.current.get(id);
};

OptionsStack.prototype.cancel = function () {
this._stack.pop();
};

OptionsStack.prototype.clear = function () {
this._stack = [];
};

module.exports = OptionsStack;
@@ -0,0 +1,72 @@
'use strict';

function TurnList() {}

TurnList.prototype.reset = function (charactersById) {
this._charactersById = charactersById;

this._turnIndex = -1;
this.turnNumber = 0;
this.activeCharacterId = null;
this.list = this._sortByInitiative();
};

TurnList.prototype.next = function () {
// Haz que calcule el siguiente turno y devuelva el resultado
// según la especificación. Recuerda que debe saltar los personajes
// muertos.
var n = this.turnNumber;
var escogido = false;
while(!escogido){
n %= this.list.length;
if(!this._charactersById[this.list[n]].isDead()){
this.activeCharacterId = this.list[n];
escogido = true;
}
n++;
}
this.turnNumber++;

var turno = {

number : this.turnNumber,
party : this._charactersById[this.activeCharacterId].party,
activeCharacterId : this.activeCharacterId
};



return turno;
};

TurnList.prototype._sortByInitiative = function () {
// Utiliza la función Array.sort(). ¡No te implementes tu propia
// función de ordenación!
var Iarray = [];
var Narray = [];

for ( var name in this._charactersById){
var esp = {};
esp.name = name;
esp.initiative = this._charactersById[name].initiative;
Iarray.push(esp);
}

Iarray.sort(function (a , b){
if(a.initiative > b.initiative){
return -1;
}
if(a.initiative < b.initiative){
return 1;
}
return 0;
});

for (var character in Iarray){
Narray.push(Iarray[character].name);
}

return Narray;
};

module.exports = TurnList;
@@ -0,0 +1,5 @@
'use strict';

module.exports.d100 = function () {
return Math.floor(Math.random() * 100) + 1;
};
@@ -0,0 +1,96 @@
'use strict';
var items = require('./items');
var Character = require('./Character');

var Effect = items.Effect;


var lib = module.exports = {
Item: items.Item,
Weapon: items.Weapon,
Scroll: items.Scroll,
Effect: Effect,
Character: Character,

weapons: {
get sword() {
return new items.Weapon('sword', 25);
},
get wand() {
return new items.Weapon('wand', 5);
},
// Implementa los colmillos y el pseudópodo
get pseudopode() {
return new items.Weapon('pseudopode', 5, new Effect({ mp: -5 }));
},
get fangs() {
return new items.Weapon('fangs', 10);
},
},

characters: {

get heroTank() {
return new Character('Tank', {
initiative: 10,
weapon: lib.weapons.sword,
defense: 70,
hp: 80,
mp: 30
});
},

get heroWizard() {
return new Character('Wizard', {
initiative: 4,
weapon: lib.weapons.wand,
defense: 50,
hp: 40,
mp: 100
});
},
// Implementa el mago

get monsterSkeleton() {
return new Character('skeleton', {
initiative: 9,
defense: 50,
weapon: lib.weapons.sword,
hp: 100,
mp: 0
});
},

get monsterSlime() {
return new Character('slime', {
initiative: 2,
defense: 40,
weapon: lib.weapons.pseudopode,
hp: 40,
mp: 50
});
},
get monsterBat() {
return new Character('bat', {
initiative: 30,
defense: 80,
weapon: lib.weapons.fangs,
hp: 5,
mp: 0
});
},
// Implementa el limo y el murciélago
},

scrolls: {

get health() {
return new items.Scroll('health', 10, new Effect({ hp: 25 }));
},
get fireball() {
return new items.Scroll('fireball', 30, new Effect({ hp: -25 }));
},
// Implementa la bola de fuego

}
};
@@ -0,0 +1,47 @@
'use strict';

function Item(name, effect) {
this.name = name;
this.effect = effect;
}

function Weapon(name, damage, extraEffect) {
this.extraEffect = extraEffect || new Effect({});
// Haz que Weapon sea subtipo de Item haciendo que llame al constructor de
// de Item.
this.extraEffect.hp = -damage;
Item.call(this, name, this.extraEffect);
}
// Termina de implementar la herencia haciendo que la propiedad prototype de
// Item sea el prototipo de Weapon.prototype y recuerda ajustar el constructor.
Weapon.prototype = Object.create(Item.prototype);
Weapon.prototype.constructor = Weapon;

function Scroll(name, cost, effect) {
Item.call(this, name, effect);
this.cost = cost;
}
Scroll.prototype = Object.create(Item.prototype);
Scroll.prototype.constructor = Scroll;

Scroll.prototype.canBeUsed = function (mp) {
// El pergamino puede usarse si los puntos de maná son superiores o iguales
// al coste del hechizo.
return mp >= this.cost;
};

function Effect(variations) {
// Copia las propiedades que se encuentran en variations como propiedades de
// este objeto.
for(var name in variations)
{
this[name] = variations[name];
}
}

module.exports = {
Item: Item,
Weapon: Weapon,
Scroll: Scroll,
Effect: Effect
};
@@ -0,0 +1,16 @@
'use strict';

module.exports = {
listToMap: function (list, getIndex) {
return list.reduce(function (map, item) {
map[getIndex(item)] = item;
return map;
}, {});
},

mapValues: function (map) {
return Object.keys(map).map(function (key) {
return map[key];
});
}
};