| @@ -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">'Los suites en Jasmine'</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span> | ||
|
|
||
| <span class="at">describe</span>(<span class="st">'pueden anidarse'</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span> | ||
|
|
||
| <span class="at">it</span>(<span class="st">'y encierran tests con expectativas'</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">'b'</span> to be <span class="st">'c'</span>. | ||
| Error<span class="op">:</span> Expected <span class="st">'b'</span> to be <span class="st">'c'</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"><</span>anonymous<span class="op">></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">'Los suites en Jasmine'</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span> | ||
|
|
||
| <span class="at">describe</span>(<span class="st">'pueden anidarse'</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span> | ||
|
|
||
| <span class="at">it</span>(<span class="st">'y encierran tests con expectativas'</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">'este test está desactivado'</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">'la suite y todos sus tests están desactivados.'</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">'events'</span>).<span class="at">EventEmitter</span><span class="op">;</span> | ||
|
|
||
| <span class="at">describe</span>(<span class="st">'EventEmitter'</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span> | ||
|
|
||
| <span class="at">it</span>(<span class="st">'emite eventos arbitrarios'</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">'turn'</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">'turn'</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" | ||
| } | ||
| } |
| @@ -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'); | ||
| }); | ||
| }); |
| @@ -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]; | ||
| }); | ||
| } | ||
| }; |