├── src ├── app │ ├── shared │ │ └── index.ts │ ├── cell │ │ ├── shared │ │ │ └── index.ts │ │ ├── cell.component.html │ │ ├── index.ts │ │ ├── cell.component.css │ │ ├── cell.component.ts │ │ └── cell.component.spec.ts │ ├── map │ │ ├── shared │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── map.component.html │ │ ├── map.component.css │ │ ├── map.component.ts │ │ └── map.component.spec.ts │ ├── console │ │ ├── shared │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── console.component.html │ │ ├── console.component.css │ │ ├── console.component.ts │ │ └── console.component.spec.ts │ ├── parser │ │ ├── shared │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── parser.component.css │ │ ├── parser.component.html │ │ ├── parser.component.ts │ │ └── parser.component.spec.ts │ ├── redux-adventure.component.css │ ├── world │ │ ├── thing.ts │ │ ├── directions.ts │ │ ├── room.spec.ts │ │ ├── dungeon.ts │ │ ├── dungeonMaster.spec.ts │ │ ├── room.ts │ │ └── dungeonMaster.ts │ ├── reducers │ │ ├── freeze.room.spec.ts │ │ ├── reducer.console.ts │ │ ├── reducer.things.ts │ │ ├── reducer.inventory.ts │ │ ├── reducer.console.spec.ts │ │ ├── reducer.room.ts │ │ ├── reducer.rooms.ts │ │ ├── reducer.inventory.spec.ts │ │ ├── reducer.things.spec.ts │ │ ├── reducer.room.spec.ts │ │ ├── reducer.rooms.spec.ts │ │ ├── reducer.main.ts │ │ └── reducer.main.spec.ts │ ├── redux-adventure.component.html │ ├── settings.ts │ ├── app.module.ts │ ├── actions │ │ ├── ActionList.ts │ │ ├── creationAction.spec.ts │ │ └── createAction.ts │ ├── redux-adventure.component.ts │ ├── seed │ │ ├── thingSeed.ts │ │ └── generatorSeed.ts │ └── redux-adventure.component.spec.ts ├── assets │ └── .gitkeep ├── favicon.ico ├── styles.css ├── environments │ ├── environment.dev.ts │ ├── environment.prod.ts │ ├── environment.js │ └── environment.ts ├── typings.d.ts ├── index.html ├── main.ts ├── tsconfig.json ├── polyfills.ts ├── test.ts └── system-config.ts ├── e2e ├── typings.d.ts ├── app.po.ts ├── app.e2e.ts └── tsconfig.json ├── Dockerfile ├── .vscode └── settings.json ├── .dockerignore ├── thumbnail.gif ├── .clang-format ├── .editorconfig ├── typings.json ├── .gitignore ├── protractor.conf.js ├── karma.conf.js ├── angular-cli.json ├── ReadMe.md ├── package.json └── tslint.json /src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/cell/shared/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/map/shared/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/console/shared/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/parser/shared/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/cell/cell.component.html: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /src/app/map/index.ts: -------------------------------------------------------------------------------- 1 | export * from './map.component'; 2 | -------------------------------------------------------------------------------- /src/app/cell/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cell.component'; 2 | -------------------------------------------------------------------------------- /src/app/parser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser.component'; 2 | -------------------------------------------------------------------------------- /e2e/typings.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/app/console/index.ts: -------------------------------------------------------------------------------- 1 | export * from './console.component'; 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | COPY dist /usr/share/nginx/html 3 | EXPOSE 80/tcp 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | config 2 | e2e 3 | node_modules 4 | public 5 | src 6 | tmp 7 | typings 8 | -------------------------------------------------------------------------------- /thumbnail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremyLikness/redux-adventure/HEAD/thumbnail.gif -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 4 | -------------------------------------------------------------------------------- /src/app/parser/parser.component.css: -------------------------------------------------------------------------------- 1 | input { 2 | width: 600px; 3 | margin-left: 5px; 4 | } -------------------------------------------------------------------------------- /src/app/redux-adventure.component.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-family: Arial, Helvetica, sans-serif 3 | } -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremyLikness/redux-adventure/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/environments/environment.dev.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare var module: { id: string }; 3 | -------------------------------------------------------------------------------- /src/app/world/thing.ts: -------------------------------------------------------------------------------- 1 | export class Thing { 2 | name: string = ''; 3 | description: string = ''; 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/console/console.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{line}}

3 |
4 | -------------------------------------------------------------------------------- /src/app/cell/cell.component.css: -------------------------------------------------------------------------------- 1 | div { 2 | width: 24px; 3 | height: 14px; 4 | float: left; 5 | background: lightblue; 6 | border: solid 2px lightblue; 7 | } -------------------------------------------------------------------------------- /src/app/parser/parser.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | export class ReduxAdventurePage { 2 | navigateTo() { 3 | return browser.get('/'); 4 | } 5 | 6 | getParagraphText() { 7 | return element(by.css('redux-adventure-app h1')).getText(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/map/map.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /src/environments/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | return { 5 | environment: environment, 6 | baseURL: '/', 7 | locationType: 'auto' 8 | }; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /src/app/world/directions.ts: -------------------------------------------------------------------------------- 1 | export enum Directions { 2 | North = 0, 3 | South, 4 | East, 5 | West 6 | } 7 | 8 | export const INVERSION_MAP: Directions[] = 9 | [Directions.South, Directions.North, Directions.West, Directions.East]; 10 | -------------------------------------------------------------------------------- /src/app/reducers/freeze.room.spec.ts: -------------------------------------------------------------------------------- 1 | import { Room } from '../world/room'; 2 | 3 | export const freezeRoom = (room: Room) => { 4 | Object.freeze(room.directions); 5 | Object.freeze(room.things); 6 | Object.freeze(room.walls); 7 | Object.freeze(room); 8 | } -------------------------------------------------------------------------------- /src/app/redux-adventure.component.html: -------------------------------------------------------------------------------- 1 |

2 | {{title}} 3 |

4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/console/console.component.css: -------------------------------------------------------------------------------- 1 | div { 2 | width: 640px; 3 | height: 480px; 4 | padding: 5px; 5 | margin: 5px; 6 | background: black; 7 | color: lightgray; 8 | overflow-y: scroll; 9 | font-family: consolas, 'Lucida Console', monospace 10 | } -------------------------------------------------------------------------------- /src/app/settings.ts: -------------------------------------------------------------------------------- 1 | export const GRID_SIZE = 10; 2 | export const CELLS = GRID_SIZE * GRID_SIZE; 3 | export const NOT_VISITED_COLOR = 'black'; 4 | export const VISITED_COLOR = 'white'; 5 | export const CURRENT_COLOR = '#aaffaa'; 6 | export const WALL_COLOR = 'black'; 7 | export const KEY_ENTER = 13; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReduxAdventure 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDevDependencies": { 3 | "angular-protractor": "registry:dt/angular-protractor#1.5.0+20160425143459", 4 | "jasmine": "registry:dt/jasmine#2.2.0+20160412134438", 5 | "selenium-webdriver": "registry:dt/selenium-webdriver#2.44.0+20160317120654" 6 | }, 7 | "ambientDependencies": { 8 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { enableProdMode } from '@angular/core'; 5 | import { environment } from './environments/environment'; 6 | import { AppModule } from './app/app.module'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule); 13 | -------------------------------------------------------------------------------- /e2e/app.e2e.ts: -------------------------------------------------------------------------------- 1 | import { ReduxAdventurePage } from './app.po'; 2 | 3 | describe('redux-adventure App', function() { 4 | let page: ReduxAdventurePage; 5 | 6 | beforeEach(() => { 7 | page = new ReduxAdventurePage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('redux-adventure works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/map/map.component.css: -------------------------------------------------------------------------------- 1 | div.grid { 2 | margin-top: 5px; 3 | width: 290px; 4 | height: 182px; 5 | background: lightgray; 6 | border: solid black 1px; 7 | text-align: center; 8 | overflow: none; 9 | } 10 | 11 | div.row { 12 | text-align: center; 13 | width: 288px; 14 | height: 16px; 15 | border: solid 1px white; 16 | background: lightgray; 17 | overflow: none; 18 | } -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.console.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | import { ITextAction } from '../actions/createAction'; 3 | import { ACTION_TEXT } from '../actions/ActionList'; 4 | 5 | export const console = (state: string[] = [], action: Action) => { 6 | 7 | if (action.type === ACTION_TEXT) { 8 | let textAction = action as ITextAction; 9 | return [...state, textAction.text]; 10 | } 11 | 12 | return [...state]; 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/world/room.spec.ts: -------------------------------------------------------------------------------- 1 | import { Directions } from './directions'; 2 | import { Room } from './room'; 3 | 4 | describe('Room', () => { 5 | it('should handle directions', 6 | () => { 7 | let room1 = new Room(), 8 | room2 = new Room(), 9 | room3 = new Room(); 10 | room1.setDirection(Directions.East, room2); 11 | room1.setDirection(Directions.South, room3); 12 | expect(room1.east).toBe(room2); 13 | expect(room1.south).toBe(room3); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "noEmitOnError": true, 10 | "noImplicitAny": false, 11 | "outDir": "../dist/", 12 | "rootDir": ".", 13 | "sourceMap": true, 14 | "target": "es5", 15 | "inlineSources": true 16 | }, 17 | 18 | "files": [ 19 | "main.ts", 20 | "typings.d.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/app/world/dungeon.ts: -------------------------------------------------------------------------------- 1 | import { Room } from './room'; 2 | import { Thing } from './thing'; 3 | 4 | export class Dungeon { 5 | rooms: Room[] = []; 6 | inventory: Thing[] = []; 7 | trophyCount: number = 0; 8 | currentRoomIdx: number = -1; 9 | public get currentRoom(): Room { 10 | if (this.currentRoomIdx < 0 || this.currentRoomIdx >= this.rooms.length) { 11 | return null; 12 | } 13 | return this.rooms[this.currentRoomIdx]; 14 | } 15 | public console: string [] = []; 16 | public won: boolean = false; 17 | } -------------------------------------------------------------------------------- /src/app/reducers/reducer.things.ts: -------------------------------------------------------------------------------- 1 | import { Thing } from '../world/thing'; 2 | import { IInventoryAction } from '../actions/createAction'; 3 | import { ACTION_GET } from '../actions/ActionList'; 4 | import { Action } from 'redux'; 5 | 6 | export const things = (state: Thing[] = [], action: Action) => { 7 | 8 | if (action.type === ACTION_GET) { 9 | let inventoryAction = action as IInventoryAction; 10 | let idx = state.indexOf(inventoryAction.item); 11 | return [...state.slice(0, idx), ...state.slice(idx + 1)]; 12 | } 13 | 14 | return state; 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.inventory.ts: -------------------------------------------------------------------------------- 1 | import { Thing } from '../world/thing'; 2 | import { IInventoryAction } from '../actions/createAction'; 3 | import { ACTION_GET } from '../actions/ActionList'; 4 | import { Action } from 'redux'; 5 | 6 | export const inventory = (state: Thing[] = [], action: Action) => { 7 | 8 | if (action.type === ACTION_GET) { 9 | let inventoryAction = action as IInventoryAction; 10 | let newThing = new Thing(); 11 | newThing.name = inventoryAction.item.name; 12 | newThing.description = inventoryAction.item.description; 13 | return [...state, newThing]; 14 | } 15 | return state; 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | 18 | # IDE - VSCode 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | # misc 26 | /.sass-cache 27 | /connect.lock 28 | /coverage/* 29 | /libpeerconnection.log 30 | npm-debug.log 31 | testem.log 32 | /typings 33 | 34 | # e2e 35 | /e2e/*.js 36 | /e2e/*.map 37 | 38 | #System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular 2 and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /src/app/console/console.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ElementRef, OnChanges, ViewChild } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'console', 5 | templateUrl: './console.component.html', 6 | styleUrls: ['./console.component.css'] 7 | }) 8 | export class ConsoleComponent implements OnChanges { 9 | 10 | private div: HTMLDivElement; 11 | 12 | @ViewChild('consoleDiv') 13 | public set consoleDiv(elem: ElementRef) { 14 | this.div = elem.nativeElement; 15 | } 16 | 17 | @Input('list') 18 | public list: string[]; 19 | 20 | constructor() { } 21 | 22 | ngOnChanges(): void { 23 | if (this.div) { 24 | setTimeout(() => this.div.scrollTop = this.div.scrollHeight, 0); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/parser/parser.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter } from '@angular/core'; 2 | import { KEY_ENTER } from '../settings'; 3 | 4 | @Component({ 5 | selector: 'parser', 6 | templateUrl: './parser.component.html', 7 | styleUrls: ['./parser.component.css'] 8 | }) 9 | export class ParserComponent { 10 | 11 | @Output('action') 12 | public action: EventEmitter = new EventEmitter(); 13 | 14 | public text: string = ''; 15 | 16 | constructor() { } 17 | 18 | public parseInput($event: any) { 19 | if ($event && $event.keyCode === KEY_ENTER) { 20 | this.enterText(); 21 | } 22 | } 23 | 24 | public enterText(): void { 25 | let command = this.text.toLowerCase().trim(); 26 | if (command) { 27 | this.action.emit(command); 28 | } 29 | this.text = ''; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | 6 | import { ReduxAdventureComponent } from './redux-adventure.component'; 7 | import { ConsoleComponent } from './console'; 8 | import { ParserComponent } from './parser'; 9 | import { MapComponent } from './map'; 10 | import { CellComponent } from './cell'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | ReduxAdventureComponent, 15 | ConsoleComponent, 16 | ParserComponent, 17 | MapComponent, 18 | CellComponent 19 | ], 20 | imports: [ 21 | BrowserModule, 22 | FormsModule, 23 | HttpModule 24 | ], 25 | providers: [], 26 | bootstrap: [ReduxAdventureComponent] 27 | }) 28 | export class AppModule { } 29 | -------------------------------------------------------------------------------- /src/app/actions/ActionList.ts: -------------------------------------------------------------------------------- 1 | export const ACTION_L = "l"; 2 | export const ACTION_LOOK = "look"; 3 | 4 | export const ACTION_N: string = "n"; 5 | export const ACTION_NORTH: string = "north"; 6 | 7 | export const ACTION_S: string = "s"; 8 | export const ACTION_SOUTH: string = "south"; 9 | 10 | export const ACTION_E: string = "e"; 11 | export const ACTION_EAST: string = "east"; 12 | 13 | export const ACTION_W: string = "w"; 14 | export const ACTION_WEST: string = "west"; 15 | 16 | 17 | export const ACTION_I: string = "i"; 18 | export const ACTION_INVENTORY: string = "inventory"; 19 | 20 | export const ACTION_G: string = "g"; 21 | 22 | // these are the actions actually composed to redux 23 | export const ACTION_MOVE: string = "move"; 24 | export const ACTION_GET: string = "get"; 25 | export const ACTION_WON: string = "won"; 26 | export const ACTION_TEXT: string = "text"; 27 | 28 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.console.spec.ts: -------------------------------------------------------------------------------- 1 | import { ITextAction } from '../actions/createAction'; 2 | import { ACTION_TEXT } from '../actions/ActionList'; 3 | import { console } from './reducer.console'; 4 | 5 | describe('console', () => { 6 | it('should do nothing for non-text actions', () => { 7 | let consoleStart = []; 8 | Object.freeze(consoleStart); 9 | expect(console(consoleStart, { type: 'TEST'})).toEqual([]); 10 | }); 11 | it('should add the text if it is a get action', () => { 12 | let newText = 'X'; 13 | let action = { 14 | type: ACTION_TEXT, 15 | text: newText 16 | } as ITextAction; 17 | 18 | let consoleStart = []; 19 | let expectedState = [newText]; 20 | 21 | Object.freeze(action); 22 | Object.freeze(consoleStart); 23 | 24 | expect(console(consoleStart, action)).toEqual(expectedState); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.room.ts: -------------------------------------------------------------------------------- 1 | import { things } from './reducer.things'; 2 | import { Room } from '../world/room'; 3 | import { IRoomAction } from '../actions/createAction'; 4 | import { ACTION_GET, ACTION_MOVE } from '../actions/ActionList'; 5 | import { Action } from 'redux'; 6 | 7 | export const room = (state: Room = new Room(), action: Action) => { 8 | 9 | let newRoom = new Room(); 10 | newRoom.idx = state.idx; 11 | newRoom.directions = [...state.directions]; 12 | newRoom.walls = [...state.walls]; 13 | newRoom.name = state.name; 14 | newRoom.description = state.description; 15 | newRoom.visited = state.visited; 16 | newRoom.things = action.type === ACTION_GET ? things(state.things, action) : [...state.things]; 17 | 18 | if (action.type === ACTION_MOVE && (action).newRoom.visited === false) { 19 | newRoom.visited = true; 20 | } 21 | 22 | return newRoom; 23 | }; 24 | -------------------------------------------------------------------------------- /src/app/console/console.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { By } from '@angular/platform-browser'; 4 | import { DebugElement, ElementRef } from '@angular/core'; 5 | import { ConsoleComponent } from './console.component'; 6 | 7 | describe('Component: Console', () => { 8 | it('should create an instance', () => { 9 | let component = new ConsoleComponent(); 10 | expect(component).toBeTruthy(); 11 | }); 12 | 13 | it('should set the scrollTop to the scrollHeight on changes', (done) => { 14 | 15 | let component = new ConsoleComponent(); 16 | let div = { 17 | scrollTop: 20, 18 | scrollHeight: 100 19 | }; 20 | let element: ElementRef = { 21 | nativeElement: div 22 | }; 23 | component.consoleDiv = element; 24 | component.ngOnChanges(); 25 | setTimeout(() => { 26 | expect(div.scrollTop).toEqual(div.scrollHeight); 27 | done(); 28 | }, 0); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/app/map/map.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Room } from '../world/room'; 3 | import { GRID_SIZE } from '../settings'; 4 | 5 | @Component({ 6 | selector: 'map', 7 | templateUrl: './map.component.html', 8 | styleUrls: ['./map.component.css'] 9 | }) 10 | export class MapComponent { 11 | 12 | public grid: Room[][] = []; 13 | 14 | @Input('currentRoom') 15 | public currentRoom: Room; 16 | 17 | @Input('rooms') 18 | public set rooms(val: Room[]) { 19 | if (val && val.length > 0) { 20 | this.grid = []; 21 | for (let northToSouth = 0; northToSouth < GRID_SIZE; northToSouth += 1) { 22 | let row: Room[] = []; 23 | for (let westToEast = 0; westToEast < GRID_SIZE; westToEast += 1) { 24 | let idx = northToSouth * GRID_SIZE + westToEast; 25 | row.push(val[idx]); 26 | } 27 | this.grid.push(row); 28 | } 29 | } 30 | } 31 | 32 | constructor() { } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/app/redux-adventure.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { createStore, Store } from 'redux'; 3 | import { Dungeon } from './world/dungeon'; 4 | import { mainReducer } from './reducers/reducer.main'; 5 | import { createAction } from './actions/createAction'; 6 | 7 | @Component({ 8 | selector: 'redux-adventure-app', 9 | templateUrl: './redux-adventure.component.html', 10 | styleUrls: ['./redux-adventure.component.css'], 11 | }) 12 | export class ReduxAdventureComponent { 13 | 14 | private store: Store; 15 | 16 | public dungeon: Dungeon; 17 | 18 | constructor() { 19 | this.store = createStore(mainReducer); 20 | this.dungeon = this.store.getState(); 21 | this.store.subscribe(() => this.dungeon = this.store.getState()); 22 | } 23 | title = 'Welcome to the Redux Adventure!'; 24 | 25 | public handleAction(action: string): void { 26 | this.store.dispatch(createAction(this.store.getState(), action)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.rooms.ts: -------------------------------------------------------------------------------- 1 | import { Room } from '../world/room'; 2 | import { IInventoryAction, IRoomAction } from '../actions/createAction'; 3 | import { ACTION_GET, ACTION_MOVE } from '../actions/ActionList'; 4 | import { room } from './reducer.room'; 5 | import { Action } from 'redux'; 6 | 7 | export const rooms = (state: Room[] = [], action: Action) => { 8 | 9 | if (action.type === ACTION_GET) { 10 | let inventoryAction = action as IInventoryAction; 11 | let idx = inventoryAction.room.idx; 12 | return [...state.slice(0, idx), room(inventoryAction.room, inventoryAction), 13 | ...state.slice(idx + 1)]; 14 | } 15 | 16 | if (action.type === ACTION_MOVE) { 17 | let moveAction = action as IRoomAction; 18 | let idx = moveAction.newRoom.idx; 19 | return [...state.slice(0, idx), room(state[moveAction.newRoom.idx], moveAction), 20 | ...state.slice(idx + 1)]; 21 | } 22 | 23 | return state; 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.inventory.spec.ts: -------------------------------------------------------------------------------- 1 | import { Thing } from '../world/thing'; 2 | import { IInventoryAction } from '../actions/createAction'; 3 | import { ACTION_GET } from '../actions/ActionList'; 4 | import { inventory } from './reducer.inventory'; 5 | 6 | describe('inventory', () => { 7 | it('should do nothing for non-inventory actions', 8 | () => { 9 | let state = []; 10 | Object.freeze(state); 11 | expect(inventory(state, { type: 'TEST'})).toEqual([]); 12 | }); 13 | it('should add the inventory if it is a get action', () => { 14 | let newThing = new Thing(); 15 | newThing.name = 'X'; 16 | let action = { 17 | type: ACTION_GET, 18 | item: newThing 19 | } as IInventoryAction; 20 | 21 | let expectedState = [newThing]; 22 | let inventoryStart = []; 23 | 24 | Object.freeze(action); 25 | Object.freeze(inventoryStart); 26 | 27 | expect(inventory(inventoryStart, action)).toEqual(expectedState); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/map/map.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { By } from '@angular/platform-browser'; 4 | import { DebugElement } from '@angular/core'; 5 | import { MapComponent } from './map.component'; 6 | import { GRID_SIZE } from '../settings'; 7 | import { Room } from '../world/room'; 8 | 9 | describe('Component: Map', () => { 10 | it('should create an instance', () => { 11 | let component = new MapComponent(); 12 | expect(component).toBeTruthy(); 13 | }); 14 | 15 | it('should generate a grid that is a matrix of GRID_SIZE x GRID_SIZE', () => { 16 | 17 | let rooms: Room[] = []; 18 | let x = GRID_SIZE * GRID_SIZE; 19 | while (x -= 1) { 20 | rooms.push(new Room()); 21 | } 22 | let component = new MapComponent(); 23 | component.rooms = rooms; 24 | expect(component.grid.length).toEqual(GRID_SIZE); 25 | for (let idx = 0; idx < component.grid.length; idx += 1) { 26 | expect(component.grid[idx].length).toEqual(GRID_SIZE); 27 | } 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | let context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.things.spec.ts: -------------------------------------------------------------------------------- 1 | import { Thing } from '../world/thing'; 2 | import { IInventoryAction } from '../actions/createAction'; 3 | import { ACTION_GET } from '../actions/ActionList'; 4 | import { things } from './reducer.things'; 5 | 6 | 7 | describe('things', () => { 8 | let thing1: Thing = null, thing2: Thing = null, items: Thing[] = []; 9 | 10 | beforeEach(() => { 11 | thing1 = new Thing(); 12 | thing1.name = 'X'; 13 | 14 | Object.freeze(thing1); 15 | 16 | thing2 = new Thing(); 17 | thing2.name = 'Y'; 18 | 19 | Object.freeze(thing2); 20 | 21 | items = [thing1, thing2]; 22 | Object.freeze(items); 23 | }); 24 | 25 | it('should do nothing for non-inventory actions', () => { 26 | expect(things(items, { type: 'TEST'})).toEqual(items); 27 | }); 28 | 29 | it('should remove the inventory if it is a get action', () => { 30 | 31 | let action = { 32 | type: ACTION_GET, 33 | item: thing1 34 | } as IInventoryAction; 35 | 36 | let expectedState = [thing2]; 37 | 38 | Object.freeze(action); 39 | 40 | expect(things(items, action)).toEqual(expectedState); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.room.spec.ts: -------------------------------------------------------------------------------- 1 | import { Thing } from '../world/thing'; 2 | import { Room } from '../world/room'; 3 | import { IInventoryAction } from '../actions/createAction'; 4 | import { ACTION_GET } from '../actions/ActionList'; 5 | import { room } from './reducer.room'; 6 | import { freezeRoom } from './freeze.room.spec'; 7 | 8 | describe('room', () => { 9 | it('should do nothing for non-inventory actions', 10 | () => { 11 | let oldRoom = new Room(), thing = new Thing(); 12 | oldRoom.things.push(thing); 13 | Object.freeze(thing); 14 | freezeRoom(oldRoom); 15 | expect(room(oldRoom, { type: 'TEST'})).toEqual(oldRoom); 16 | }); 17 | it('should remove the inventory if it is a get action', () => { 18 | let oldRoom = new Room(); 19 | let newRoom = new Room(); 20 | let oldThing = new Thing(); 21 | oldThing.name = 'X'; 22 | oldRoom.things.push(oldThing); 23 | let action = { 24 | type: ACTION_GET, 25 | item: oldThing 26 | } as IInventoryAction; 27 | 28 | Object.freeze(oldThing); 29 | freezeRoom(oldRoom); 30 | Object.freeze(action); 31 | 32 | expect(room(oldRoom, action)).toEqual(newRoom); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', 'angular-cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-remap-istanbul'), 12 | require('angular-cli/plugins/karma') 13 | ], 14 | files: [ 15 | { pattern: './src/test.ts', watched: false } 16 | ], 17 | preprocessors: { 18 | './src/test.ts': ['angular-cli'] 19 | }, 20 | mime: { 21 | 'text/x-typescript': ['ts','tsx'] 22 | }, 23 | remapIstanbulReporter: { 24 | reports: { 25 | html: 'coverage', 26 | lcovonly: './coverage/coverage.lcov' 27 | } 28 | }, 29 | angularCli: { 30 | config: './angular-cli.json', 31 | environment: 'dev' 32 | }, 33 | reporters: config.angularCli && config.angularCli.codeCoverage 34 | ? ['progress', 'karma-remap-istanbul'] 35 | : ['progress'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/app/seed/thingSeed.ts: -------------------------------------------------------------------------------- 1 | export interface IThing { 2 | name: string; 3 | description: string; 4 | } 5 | 6 | export const THING_SEED: IThing[] = [ 7 | { 8 | 'name': 'Rusty Sword', 9 | 'description': 'a rusty sword with a pitted blade and worn hilt' 10 | }, 11 | { 12 | 'name': 'Sparkling Diamond', 13 | 'description': 'a beautiful, perfectly clear diamond about the size of your fist' 14 | }, 15 | { 16 | 'name': 'Commodore 64', 17 | 'description': 18 | 'a classic Commdore 64 \'bread box\' complete with a ' + 19 | 'joy stick and small portable RCA television' 20 | }, 21 | { 22 | 'name': 'Fortune Cookie Wrapper', 23 | 'description': 'a small, curled fortune cookie wrapper with the phrase ' + 24 | '\'All your base are belong to us\' printed on it' 25 | }, 26 | { 27 | 'name': 'Ultima Series Boxed Set', 28 | 'description': 'a boxed set of all of the Ultima games' 29 | }, 30 | { 31 | 'name': 'Nokia Lumia 1020', 32 | 'description': 'a gorgeous antique yellow smart phone ' + 33 | 'with beautiful camera and crisp display' 34 | }, 35 | { 36 | 'name': 'Conundrum', 37 | 'description': 'something that is puzzling or confusing' 38 | } 39 | ]; 40 | 41 | -------------------------------------------------------------------------------- /angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "1.0.0-beta.24", 4 | "name": "redux-adventure" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "test": "test.ts", 17 | "tsconfig": "tsconfig.json", 18 | "prefix": "app", 19 | "mobile": false, 20 | "styles": [ 21 | "styles.css" 22 | ], 23 | "scripts": [], 24 | "environments": { 25 | "source": "environments/environment.ts", 26 | "dev": "environments/environment.ts", 27 | "prod": "environments/environment.prod.ts" 28 | } 29 | } 30 | ], 31 | "addons": [], 32 | "packages": [], 33 | "e2e": { 34 | "protractor": { 35 | "config": "./protractor.conf.js" 36 | } 37 | }, 38 | "test": { 39 | "karma": { 40 | "config": "./karma.conf.js" 41 | } 42 | }, 43 | "defaults": { 44 | "styleExt": "css", 45 | "prefixInterfaces": false, 46 | "inline": { 47 | "style": false, 48 | "template": false 49 | }, 50 | "spec": { 51 | "class": false, 52 | "component": true, 53 | "directive": true, 54 | "module": false, 55 | "pipe": true, 56 | "service": true 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # redux-adventure 2 | 3 | Redux Adventure is a text-based adventure game written in Angular 2 and TypeScript that leverages Redux. It is a simple implementation designed to both learn and teach the principles of an app that relies on Redux for state management. 4 | 5 | [Read the blog post](http://csharperimage.jeremylikness.com/2016/07/an-adventure-in-redux-building-redux.html) to learn more about how the app is organized, structured, and the role Redux plays in driving gameplay. 6 | 7 | ![Redux Adventure](./thumbnail.gif) 8 | 9 | ## Quick Start 10 | 11 | ### Online 12 | [Play the Game](https://jeremylikness.github.io/redux-adventure/) 13 | 14 | ### Local 15 | 16 | First, clone the repo: 17 | 18 | `git clone https://github.com/JeremyLikness/redux-adventure.git` 19 | 20 | `cd redux-adventure` 21 | 22 | Next, install the dependencies: 23 | `npm install` 24 | 25 | The project uses the [Angular-CLI](http://developer.telerik.com/featured/rapid-cross-platform-development-angular-2-cli/): 26 | 27 | `npm i -g angular-cli` 28 | 29 | Now you can launch it: 30 | 31 | `ng serve` 32 | 33 | ## Objective 34 | 35 | The objective is to explore the dungeon and pick up all of the artifacts. You win the game when your inventory includes all possible items. The game will render a map of areas you've visited. The following commands are possible: 36 | 37 | `n north s south e east w west g get i inventory` 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-adventure", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "lint": "tslint \"src/**/*.ts\"", 10 | "test": "ng test", 11 | "pree2e": "webdriver-manager update --standalone false --gecko false", 12 | "e2e": "protractor" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "^2.3.1", 17 | "@angular/compiler": "^2.3.1", 18 | "@angular/core": "^2.3.1", 19 | "@angular/forms": "^2.3.1", 20 | "@angular/http": "^2.3.1", 21 | "@angular/platform-browser": "^2.3.1", 22 | "@angular/platform-browser-dynamic": "^2.3.1", 23 | "@angular/router": "^3.3.1", 24 | "core-js": "^2.4.1", 25 | "redux": "^3.6.0", 26 | "rxjs": "^5.0.1", 27 | "ts-helpers": "^1.1.1", 28 | "zone.js": "^0.7.2" 29 | }, 30 | "devDependencies": { 31 | "@angular/compiler-cli": "^2.3.1", 32 | "@types/jasmine": "2.5.38", 33 | "@types/node": "^6.0.42", 34 | "angular-cli": "1.0.0-beta.24", 35 | "codelyzer": "~2.0.0-beta.1", 36 | "jasmine-core": "2.5.2", 37 | "jasmine-spec-reporter": "2.5.0", 38 | "karma": "1.2.0", 39 | "karma-chrome-launcher": "^2.0.0", 40 | "karma-cli": "^1.0.1", 41 | "karma-jasmine": "^1.0.2", 42 | "karma-remap-istanbul": "^0.2.1", 43 | "protractor": "~4.0.13", 44 | "ts-node": "1.2.1", 45 | "tslint": "^4.0.2", 46 | "typescript": "~2.0.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/cell/cell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewChild, ElementRef, OnChanges } from '@angular/core'; 2 | import { Room } from '../world/room'; 3 | import { 4 | NOT_VISITED_COLOR, 5 | VISITED_COLOR, 6 | CURRENT_COLOR, 7 | WALL_COLOR 8 | } from '../settings'; 9 | 10 | @Component({ 11 | selector: 'cell', 12 | templateUrl: './cell.component.html', 13 | styleUrls: ['./cell.component.css'] 14 | }) 15 | export class CellComponent implements OnChanges { 16 | 17 | private div: HTMLDivElement; 18 | 19 | @ViewChild('cellDiv') 20 | public set cellDiv(val: ElementRef) { 21 | this.div = val.nativeElement; 22 | this.processStyle(); 23 | } 24 | 25 | @Input('room') 26 | public room: Room; 27 | 28 | @Input('isCurrentRoom') 29 | public isCurrentRoom: boolean; 30 | 31 | constructor() { } 32 | 33 | public ngOnChanges(): void { 34 | if (this.div && this.room) { 35 | this.processStyle(); 36 | } 37 | } 38 | 39 | private processStyle(): void { 40 | if (!this.room || !this.room.visited) { 41 | this.div.style.background = NOT_VISITED_COLOR; 42 | return; 43 | } 44 | this.div.style.background = this.isCurrentRoom ? CURRENT_COLOR : VISITED_COLOR; 45 | if (this.room) { 46 | if (this.room.west === null) { 47 | this.div.style.borderLeftColor = WALL_COLOR; 48 | } 49 | if (this.room.north === null) { 50 | this.div.style.borderTopColor = WALL_COLOR; 51 | } 52 | if (this.room.south === null) { 53 | this.div.style.borderBottomColor = WALL_COLOR; 54 | } 55 | if (this.room.east === null) { 56 | this.div.style.borderRightColor = WALL_COLOR; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.rooms.spec.ts: -------------------------------------------------------------------------------- 1 | import { Thing } from '../world/thing'; 2 | import { Room } from '../world/room'; 3 | import { IInventoryAction } from '../actions/createAction'; 4 | import { ACTION_GET } from '../actions/ActionList'; 5 | import { rooms } from './reducer.rooms'; 6 | import { freezeRoom } from './freeze.room.spec'; 7 | 8 | describe('rooms', () => { 9 | let room1: Room = null, room2: Room = null, room3: Room = null; 10 | let thing1: Thing = null, thing2: Thing = null, roomList: Room[] = []; 11 | 12 | beforeEach(() => { 13 | thing1 = new Thing(); 14 | thing1.name = 'X'; 15 | thing2 = new Thing(); 16 | thing2.name = 'Y'; 17 | room1 = new Room(); 18 | room1.things.push(thing1); 19 | room2 = new Room(); 20 | room2.things.push(thing2); 21 | room3 = new Room(); 22 | roomList = [room1, room2, room3]; 23 | 24 | Room.setIds(roomList); 25 | 26 | Object.freeze(thing1); 27 | Object.freeze(thing2); 28 | freezeRoom(room1); 29 | freezeRoom(room2); 30 | freezeRoom(room3); 31 | Object.freeze(roomList); 32 | }); 33 | 34 | it('should do nothing for non-inventory actions', () => { 35 | expect(rooms(roomList, { type: 'TEST'})).toEqual(roomList); 36 | }); 37 | 38 | it('should remove the inventory if it is a get action', () => { 39 | let action = { 40 | type: ACTION_GET, 41 | item: thing2, 42 | room: room2 43 | } as IInventoryAction; 44 | 45 | let room2empty = new Room(); 46 | room2empty.idx = 1; 47 | let expectedState = [room1, room2empty, room3]; 48 | 49 | Object.freeze(action); 50 | 51 | expect(rooms(roomList, action)).toEqual(expectedState); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/system-config.ts: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | * User Configuration. 3 | **********************************************************************************************/ 4 | /** Map relative paths to URLs. */ 5 | const map: any = { 6 | 'redux' : 'vendor/redux/dist/redux.js' 7 | }; 8 | 9 | /** User packages configuration. */ 10 | const packages: any = { 11 | } 12 | 13 | //////////////////////////////////////////////////////////////////////////////////////////////// 14 | /*********************************************************************************************** 15 | * Everything underneath this line is managed by the CLI. 16 | **********************************************************************************************/ 17 | const barrels: string[] = [ 18 | // Angular specific barrels. 19 | '@angular/core', 20 | '@angular/common', 21 | '@angular/compiler', 22 | '@angular/http', 23 | '@angular/router', 24 | '@angular/platform-browser', 25 | '@angular/platform-browser-dynamic', 26 | 27 | // Thirdparty barrels. 28 | 'rxjs', 29 | 30 | // App specific barrels. 31 | 'app', 32 | 'app/shared', 33 | 'app/console', 34 | 'app/parser', 35 | 'app/cell', 36 | 'app/map', 37 | /** @cli-barrel */ 38 | ]; 39 | 40 | const cliSystemConfigPackages: any = {}; 41 | barrels.forEach((barrelName: string) => { 42 | cliSystemConfigPackages[barrelName] = { main: 'index' }; 43 | }); 44 | 45 | /** Type declaration for ambient System. */ 46 | declare var System: any; 47 | 48 | // Apply the CLI SystemJS configuration. 49 | System.config({ 50 | map: { 51 | '@angular': 'vendor/@angular', 52 | 'rxjs': 'vendor/rxjs', 53 | 'main': 'main.js' 54 | }, 55 | packages: cliSystemConfigPackages 56 | }); 57 | 58 | // Apply the user's configuration. 59 | System.config({ map, packages }); 60 | -------------------------------------------------------------------------------- /src/app/redux-adventure.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | 5 | import { BrowserModule } from '@angular/platform-browser'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { HttpModule } from '@angular/http'; 8 | 9 | import { ReduxAdventureComponent } from './redux-adventure.component'; 10 | import { ConsoleComponent } from './console'; 11 | import { ParserComponent } from './parser'; 12 | import { MapComponent } from './map'; 13 | import { CellComponent } from './cell'; 14 | 15 | describe('AppComponent', () => { 16 | beforeEach(() => { 17 | TestBed.configureTestingModule({ 18 | declarations: [ 19 | ReduxAdventureComponent, 20 | ConsoleComponent, 21 | ParserComponent, 22 | MapComponent, 23 | CellComponent 24 | ], 25 | imports: [ 26 | BrowserModule, 27 | FormsModule, 28 | HttpModule 29 | ], 30 | }); 31 | TestBed.compileComponents(); 32 | }); 33 | 34 | it('should create the app', async(() => { 35 | let fixture = TestBed.createComponent(ReduxAdventureComponent); 36 | let app = fixture.debugElement.componentInstance; 37 | expect(app).toBeTruthy(); 38 | })); 39 | 40 | it(`should have as title 'Welcome to the Redux Adventure!'`, async(() => { 41 | let fixture = TestBed.createComponent(ReduxAdventureComponent); 42 | let app = fixture.debugElement.componentInstance; 43 | expect(app.title).toEqual('Welcome to the Redux Adventure!'); 44 | })); 45 | 46 | it('should render title in a h1 tag', async(() => { 47 | let fixture = TestBed.createComponent(ReduxAdventureComponent); 48 | fixture.detectChanges(); 49 | let compiled = fixture.debugElement.nativeElement; 50 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to the Redux Adventure!'); 51 | })); 52 | }); 53 | -------------------------------------------------------------------------------- /src/app/parser/parser.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { By } from '@angular/platform-browser'; 4 | import { DebugElement } from '@angular/core'; 5 | import { ParserComponent } from './parser.component'; 6 | import { KEY_ENTER } from '../settings'; 7 | 8 | const INPUT = ' Testing '; 9 | const COMMAND: string = INPUT.toLowerCase().trim(); 10 | 11 | describe('Component: Parser', () => { 12 | it('should create an instance', () => { 13 | let component = new ParserComponent(); 14 | expect(component).toBeTruthy(); 15 | }); 16 | 17 | it('should ignore empty text', () => { 18 | let component = new ParserComponent(); 19 | let sub = component.action.subscribe(() => { 20 | throw new Error('Test failed - should not emit an event.'); 21 | }); 22 | component.enterText(); 23 | sub.unsubscribe(); 24 | }); 25 | 26 | it('should emit a command when enterText() is fired', (done) => { 27 | let component = new ParserComponent(); 28 | component.text = INPUT; 29 | let sub = component.action.subscribe((command) => { 30 | expect(command).toEqual(COMMAND); 31 | done(); 32 | }); 33 | component.enterText(); 34 | sub.unsubscribe(); 35 | }); 36 | 37 | it('should do nothing on non-ENTER key presses', () => { 38 | let component = new ParserComponent(); 39 | component.text = INPUT; 40 | let sub = component.action.subscribe(() => { 41 | throw new Error('Test failed - should not emit an event.'); 42 | }); 43 | component.parseInput({keyCode: -1}); 44 | sub.unsubscribe(); 45 | }); 46 | 47 | it('should emit a command when ENTER is pressed', (done) => { 48 | let component = new ParserComponent(); 49 | component.text = INPUT; 50 | let sub = component.action.subscribe((command) => { 51 | expect(command).toEqual(COMMAND); 52 | done(); 53 | }); 54 | component.parseInput({keyCode: KEY_ENTER}); 55 | sub.unsubscribe(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "max-line-length": [true, 100], 5 | "no-inferrable-types": true, 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "eofline": true, 16 | "no-duplicate-variable": true, 17 | "no-eval": true, 18 | "no-arg": true, 19 | "no-internal-module": true, 20 | "no-trailing-whitespace": true, 21 | "no-bitwise": true, 22 | "no-shadowed-variable": true, 23 | "no-unused-expression": true, 24 | "no-unused-variable": true, 25 | "one-line": [ 26 | true, 27 | "check-catch", 28 | "check-else", 29 | "check-open-brace", 30 | "check-whitespace" 31 | ], 32 | "quotemark": [ 33 | true, 34 | "single", 35 | "avoid-escape" 36 | ], 37 | "semicolon": [true, "always"], 38 | "typedef-whitespace": [ 39 | true, 40 | { 41 | "call-signature": "nospace", 42 | "index-signature": "nospace", 43 | "parameter": "nospace", 44 | "property-declaration": "nospace", 45 | "variable-declaration": "nospace" 46 | } 47 | ], 48 | "curly": true, 49 | "variable-name": [ 50 | true, 51 | "ban-keywords", 52 | "check-format", 53 | "allow-trailing-underscore" 54 | ], 55 | "whitespace": [ 56 | true, 57 | "check-branch", 58 | "check-decl", 59 | "check-operator", 60 | "check-separator", 61 | "check-type" 62 | ], 63 | "component-selector-name": [true, "kebab-case"], 64 | "component-selector-type": [true, "element"], 65 | "host-parameter-decorator": true, 66 | "input-parameter-decorator": true, 67 | "output-parameter-decorator": true, 68 | "attribute-parameter-decorator": true, 69 | "input-property-directive": true, 70 | "output-property-directive": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/app/world/dungeonMaster.spec.ts: -------------------------------------------------------------------------------- 1 | import { Dungeon } from './dungeon'; 2 | import { DUNGEON_MASTER } from './dungeonMaster'; 3 | import { THING_SEED } from '../seed/thingSeed'; 4 | import { CELLS } from '../settings'; 5 | 6 | describe('Room', () => { 7 | let dm: Dungeon = null; 8 | beforeEach(() => { 9 | dm = DUNGEON_MASTER(); 10 | }); 11 | it('should generate ' + CELLS + ' rooms', 12 | () => { 13 | expect(dm).not.toBeNull(); 14 | expect(dm.rooms).not.toBeNull(); 15 | expect(dm.rooms.length).toBe(CELLS); 16 | }); 17 | it('should ensure every room has at most two walls', () => { 18 | for (let idx = 0; idx < dm.rooms.length; idx += 1) { 19 | let room = dm.rooms[idx]; 20 | expect(room.walls.length).toBeLessThan(3); 21 | } 22 | }); 23 | it('should place all artifacts in the rooms', () => { 24 | let artifacts = {}; 25 | for (let idx = 0; idx < THING_SEED.length; idx += 1) { 26 | artifacts[THING_SEED[idx].name] = false; 27 | } 28 | for (let idx = 0; idx < dm.rooms.length; idx += 1) { 29 | let room = dm.rooms[idx]; 30 | for (let innerIdx = 0; innerIdx < room.things.length; innerIdx += 1) { 31 | let thing = room.things[innerIdx]; 32 | artifacts[thing.name] = true; 33 | } 34 | } 35 | for (let idx = 0; idx < THING_SEED.length; idx += 1) { 36 | expect(artifacts[THING_SEED[idx].name]).toBe(true); 37 | } 38 | }); 39 | it('should pick a current random room', () => { 40 | expect(dm.currentRoomIdx).toBeGreaterThan(-1); 41 | expect(dm.currentRoomIdx).toBeLessThan(dm.rooms.length); 42 | }); 43 | it('should return the current room based on the index', () => { 44 | expect(dm.currentRoom).toBe(dm.rooms[dm.currentRoomIdx]); 45 | }); 46 | it('should add the current room text to the console', () => { 47 | expect(dm.console.length).toBe(1); 48 | expect(dm.console[0]).toEqual(dm.currentRoom.longDescription); 49 | }); 50 | it('should set the trophy count', () => { 51 | expect(dm.trophyCount).toBe(THING_SEED.length); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/app/world/room.ts: -------------------------------------------------------------------------------- 1 | import { Directions } from './directions'; 2 | import { Thing } from './thing'; 3 | 4 | export class Room { 5 | 6 | public directions: Room[] = [null, null, null, null]; 7 | public walls: Directions[] = []; 8 | public name: string = ''; 9 | public description: string = ''; 10 | public idx: number = -1; 11 | public visited: boolean = false; 12 | 13 | public static setIds(rooms: Room[]): void { 14 | for (let idx = 0; idx < rooms.length; idx += 1) { 15 | rooms[idx].idx = idx; 16 | } 17 | } 18 | 19 | public get longDescription(): string { 20 | let text = this.name + ': ' + this.description + '\r\n'; 21 | if (this.things.length > 0) { 22 | text += 'You see '; 23 | let descriptions = this.things.map(thing => thing.description); 24 | text += descriptions.join(', and '); 25 | text += ' on the floor.\r\n'; 26 | } 27 | let exits: Directions[] = []; 28 | for (let idx = 0; idx < this.directions.length; idx += 1) { 29 | if (this.directions[idx] !== null) { 30 | exits.push(idx); 31 | } 32 | } 33 | if (exits.length == 1) { 34 | text += 'You see an exit to the ' + Directions[exits[0]]; 35 | } 36 | else { 37 | let directionsText = exits.map(exit => Directions[exit]); 38 | text += 'You see exits in the directions: '; 39 | text += directionsText.join(', '); 40 | } 41 | return text; 42 | } 43 | 44 | public setDirection(dir: Directions, room: Room): void { 45 | this.directions[dir] = room; 46 | } 47 | 48 | public getDirection(dir): Room { 49 | return this.directions[dir]; 50 | } 51 | 52 | public get north(): Room { 53 | return this.directions[Directions.North]; 54 | } 55 | 56 | public get south(): Room { 57 | return this.directions[Directions.South]; 58 | } 59 | 60 | public get east(): Room { 61 | return this.directions[Directions.East]; 62 | } 63 | 64 | public get west(): Room { 65 | return this.directions[Directions.West]; 66 | } 67 | 68 | public things: Thing[] = []; 69 | } 70 | -------------------------------------------------------------------------------- /src/app/cell/cell.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { By } from '@angular/platform-browser'; 4 | import { DebugElement, ElementRef } from '@angular/core'; 5 | import { CellComponent } from './cell.component'; 6 | import { Room } from '../world/room'; 7 | import { Directions } from '../world/directions'; 8 | import { 9 | NOT_VISITED_COLOR, 10 | VISITED_COLOR, 11 | CURRENT_COLOR, 12 | WALL_COLOR 13 | } from '../settings'; 14 | 15 | describe('Component: Cell', () => { 16 | let element: ElementRef = null; 17 | let div = null; 18 | let room: Room = null; 19 | let component: CellComponent = null; 20 | it('should create an instance', () => { 21 | component = new CellComponent(); 22 | expect(component).toBeTruthy(); 23 | }); 24 | describe('processStyle', () => { 25 | beforeEach(() => { 26 | room = new Room(); 27 | div = { 28 | style: { 29 | background: null, 30 | borderLeftColor: null, 31 | borderTopColor: null, 32 | borderBottomColor: null, 33 | borderRightColor: null 34 | } 35 | }; 36 | element = { 37 | nativeElement: div 38 | }; 39 | component = new CellComponent(); 40 | }); 41 | 42 | it ('should set background to the not visited color', () => { 43 | component.room = room; 44 | component.cellDiv = element; 45 | expect(div.style.background).toBe(NOT_VISITED_COLOR); 46 | }); 47 | 48 | it('should set background to current color when room is current', () => { 49 | room.visited = true; 50 | component.room = room; 51 | component.isCurrentRoom = true; 52 | component.cellDiv = element; 53 | expect(div.style.background).toBe(CURRENT_COLOR); 54 | }); 55 | 56 | it('should set background to visited color when room has been visited', () => { 57 | room.visited = true; 58 | component.room = room; 59 | component.cellDiv = element; 60 | expect(div.style.background).toBe(VISITED_COLOR); 61 | }); 62 | 63 | it('should not set the wall color for directions that are not walls', () => { 64 | room.visited = true; 65 | room.setDirection(Directions.West, room); 66 | room.setDirection(Directions.South, room); 67 | component.room = room; 68 | component.cellDiv = element; 69 | expect(div.style.borderTopColor).toBe(WALL_COLOR); 70 | expect(div.style.borderRightColor).toBe(WALL_COLOR); 71 | expect(div.style.borderLeftColor).toBe(null); 72 | expect(div.style.borderBottomColor).toBe(null); 73 | }); 74 | 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/app/seed/generatorSeed.ts: -------------------------------------------------------------------------------- 1 | export interface HasDescription { 2 | description: string; 3 | } 4 | 5 | export class GeneratorSeed { 6 | public rooms: HasDescription[] = [{ 7 | 'description' : 'rusty' 8 | }, { 9 | 'description' : 'dusty' 10 | }, { 11 | 'description' : 'moldy' 12 | }, { 13 | 'description' : 'damp' 14 | }, { 15 | 'description' : 'dark' 16 | }, { 17 | 'description' : 'well-lit' 18 | }, { 19 | 'description' : 'small' 20 | }, { 21 | 'description' : 'large' 22 | }, { 23 | 'description' : 'cramped' 24 | }, { 25 | 'description' : 'spacious' 26 | }, { 27 | 'description' : 'cramped' 28 | }, { 29 | 'description' : 'clean' 30 | }, { 31 | 'description' : 'comfortable' 32 | }, { 33 | 'description' : 'smelly' 34 | }, { 35 | 'description' : 'warm' 36 | }, { 37 | 'description' : 'hot' 38 | }, { 39 | 'description' : 'cold' 40 | }]; 41 | 42 | public walls: HasDescription[] = [{ 43 | 'description' : 'dark stone walls' 44 | }, { 45 | 'description' : 'rough, rocky walls' 46 | }, { 47 | 'description' : 'soft walls of packed dirt' 48 | }, { 49 | 'description' : 'crumbling brick walls' 50 | }, { 51 | 'description' : 'walls made of bright red bricks' 52 | }, { 53 | 'description' : 'smooth black walls with a glassy texture' 54 | }, { 55 | 'description' : 'stone walls filled with exposed fossils' 56 | }, { 57 | 'description' : 'walls made of large fitted concrete blocks' 58 | }, { 59 | 'description' : 'sandstone walls' 60 | }, { 61 | 'description' : 'rocky walls with bands of color' 62 | }, { 63 | 'description' : 'rotting wallpaper with an unrecognizeable pattern' 64 | } 65 | ]; 66 | 67 | public features: HasDescription[] = [{ 68 | 'description' : 'There is a crude table in the middle of the room.' 69 | }, { 70 | 'description' : 'In the center of the room stands a dry fountain.' 71 | }, { 72 | 'description' : 'There is broken pottery scattered across the floor.' 73 | }, { 74 | 'description' : 'In the middle of the room is large pile of rust.' 75 | }, { 76 | 'description' : 'A rotting cot is tucked into the corner.' 77 | }, { 78 | 'description' : 'In the center of the floor is small drain.' 79 | }, { 80 | 'description' : 'The room is encircled by flickering torches in sconces on the walls.' 81 | }, { 82 | 'description' : 'The floor is covered with moldy hay.' 83 | }, { 84 | 'description' : 'In the center of the room is a deep, dark well with no chain or bucket.' 85 | }, { 86 | 'description' : 'There are no other features of note.' 87 | }, { 88 | 'description' : 'The room is otherwise empty.' 89 | }, { 90 | 'description' : 'You notice nothing else interesting about the room.' 91 | }]; 92 | }; 93 | -------------------------------------------------------------------------------- /src/app/actions/creationAction.spec.ts: -------------------------------------------------------------------------------- 1 | import { Directions } from '../world/directions'; 2 | import { Room } from '../world/room'; 3 | import { Dungeon } from '../world/dungeon'; 4 | import { Thing } from '../world/thing'; 5 | import { createAction } from './createAction'; 6 | import { 7 | ACTION_WEST, 8 | ACTION_EAST, 9 | ACTION_LOOK, 10 | ACTION_TEXT, 11 | ACTION_GET, 12 | ACTION_WON, 13 | ACTION_MOVE 14 | } from './ActionList'; 15 | 16 | describe('createAction', () => { 17 | 18 | let dm: Dungeon = null, room1: Room = null, room2: Room = null; 19 | 20 | beforeEach(() => { 21 | dm = new Dungeon(); 22 | room1 = new Room(); 23 | room2 = new Room(); 24 | dm.rooms.push(room1); 25 | dm.rooms.push(room2); 26 | room1.setDirection(Directions.East, room2); 27 | room2.setDirection(Directions.West, room1); 28 | dm.currentRoomIdx = 0; 29 | }); 30 | 31 | describe('directional action', () => { 32 | it('should return the text action if a wall exists', () => { 33 | expect(createAction(dm, ACTION_WEST).type).toEqual(ACTION_TEXT); 34 | }); 35 | it('should return a directional action with the new room if an exit exists', () => { 36 | expect(createAction(dm, ACTION_EAST)).toEqual({ 37 | type: ACTION_MOVE, 38 | direction: Directions.East, 39 | newRoom: room2 40 | }); 41 | }); 42 | }); 43 | 44 | describe('look', () => { 45 | it('should return the long description of the current room', () => { 46 | expect(createAction(dm, ACTION_LOOK)).toEqual({ 47 | type: ACTION_TEXT, 48 | text: room1.longDescription 49 | }); 50 | }); 51 | }); 52 | 53 | describe('inventory', () => { 54 | it('should return the text of the inventory', () => { 55 | expect(createAction(dm, ACTION_LOOK).type).toEqual(ACTION_TEXT); 56 | }); 57 | }); 58 | 59 | describe('get', () => { 60 | it('should return a text action if the room is empty', () => { 61 | expect(createAction(dm, ACTION_GET).type).toEqual(ACTION_TEXT); 62 | }); 63 | it('should return a get action if there are other items remaining in the dungeon', () => { 64 | dm.currentRoom.things.push(new Thing()); 65 | expect(createAction(dm, ACTION_GET).type).toEqual(ACTION_GET); 66 | }); 67 | it('should return the won action if the item is the final one', () => { 68 | dm.currentRoom.things.push(new Thing()); 69 | dm.trophyCount = 1; 70 | expect(createAction(dm, ACTION_GET).type).toEqual(ACTION_WON); 71 | }); 72 | }); 73 | 74 | describe('unknown', () => { 75 | it('should return a text action', () => { 76 | expect(createAction(dm, 'Random stuff').type).toEqual(ACTION_TEXT); 77 | }); 78 | }); 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.main.ts: -------------------------------------------------------------------------------- 1 | import { Dungeon } from '../world/dungeon'; 2 | import { DUNGEON_MASTER } from '../world/dungeonMaster'; 3 | import { Directions } from '../world/directions'; 4 | import { 5 | ITextAction, 6 | IWonAction, 7 | IRoomAction, 8 | IInventoryAction 9 | } from '../actions/createAction'; 10 | import { ACTION_TEXT, ACTION_MOVE, ACTION_GET, ACTION_WON } from '../actions/ActionList'; 11 | import { console } from './reducer.console'; 12 | import { inventory } from './reducer.inventory'; 13 | import { rooms } from './reducer.rooms'; 14 | import { Action } from 'redux'; 15 | 16 | export const mainReducer = (state: Dungeon = DUNGEON_MASTER(), action: Action) => { 17 | 18 | if (action.type === ACTION_TEXT) { 19 | return defaultReducer(state, action as ITextAction); 20 | } 21 | 22 | if (action.type === ACTION_MOVE) { 23 | return moveReducer(state, action as IRoomAction); 24 | } 25 | 26 | if (action.type === ACTION_GET) { 27 | return inventoryReducer(state, action as IInventoryAction); 28 | } 29 | 30 | if (action.type === ACTION_WON) { 31 | return wonReducer(state, action as IWonAction); 32 | } 33 | 34 | return state; 35 | }; 36 | 37 | const defaultReducer = (state: Dungeon, action: Action) => { 38 | let dungeon = new Dungeon(); 39 | dungeon.console = console(state.console, action); 40 | dungeon.currentRoomIdx = state.currentRoomIdx; 41 | dungeon.inventory = inventory(state.inventory, action); 42 | dungeon.trophyCount = state.trophyCount; 43 | dungeon.won = state.won; 44 | dungeon.rooms = rooms(state.rooms, action); 45 | return dungeon; 46 | }; 47 | 48 | const moveReducer = (state: Dungeon, action: IRoomAction) => { 49 | let newState = defaultReducer(state, action); 50 | newState.console = console(newState.console, { 51 | type: ACTION_TEXT, 52 | text: 'You move ' + Directions[action.direction] + '.' 53 | } as ITextAction); 54 | let newRoom = newState.currentRoom.directions[action.direction]; 55 | newState.currentRoomIdx = newRoom.idx; 56 | newState.console.push(newState.currentRoom.longDescription); 57 | return newState; 58 | }; 59 | 60 | const inventoryReducer = (state: Dungeon, action: IInventoryAction) => { 61 | let newState = defaultReducer(state, action); 62 | newState.console.push('You pick up the ' + action.item.name + '.'); 63 | return newState; 64 | }; 65 | 66 | const wonReducer = (state: Dungeon, action: IWonAction) => { 67 | let invAction: IInventoryAction = { 68 | type: ACTION_GET, 69 | item: action.item, 70 | room: action.room 71 | }; 72 | let newState = defaultReducer(state, invAction); 73 | newState.won = true; 74 | newState.console.push('You pick up the ' + action.item.name); 75 | newState.console.push('There is a blinding flash of light and you are lifted off the ground. ' + 76 | 'A booming voice sounds from all around you, declaring, "You have won!"'); 77 | return newState; 78 | }; 79 | -------------------------------------------------------------------------------- /src/app/reducers/reducer.main.spec.ts: -------------------------------------------------------------------------------- 1 | import { Dungeon } from '../world/dungeon'; 2 | import { Thing } from '../world/thing'; 3 | import { Room } from '../world/room'; 4 | import { Directions } from '../world/directions'; 5 | import { IInventoryAction, IRoomAction, IWonAction } from '../actions/createAction'; 6 | import { ACTION_TEXT, ACTION_MOVE, ACTION_GET, ACTION_WON } from '../actions/ActionList'; 7 | import { ITextAction } from '../actions/createAction'; 8 | import { mainReducer } from './reducer.main'; 9 | import { freezeRoom } from './freeze.room.spec'; 10 | 11 | describe('main', () => { 12 | let dungeon: Dungeon = null, 13 | room1: Room = null, 14 | room2: Room = null, 15 | thing1: Thing = null; 16 | beforeEach(() => { 17 | dungeon = new Dungeon(); 18 | room1 = new Room(); 19 | room2 = new Room(); 20 | thing1 = new Thing(); 21 | thing1.name = 'Magic test thing'; 22 | room1.name = 'Room 1'; 23 | room2.name = 'Room 2'; 24 | room2.things.push(thing1); 25 | room1.setDirection(Directions.East, room2); 26 | room2.setDirection(Directions.West, room1); 27 | dungeon.rooms.push(room1); 28 | dungeon.rooms.push(room2); 29 | dungeon.currentRoomIdx = 0; 30 | dungeon.trophyCount = 2; 31 | 32 | Room.setIds(dungeon.rooms); 33 | 34 | Object.freeze(thing1); 35 | freezeRoom(room1); 36 | freezeRoom(room2); 37 | Object.freeze(dungeon.console); 38 | Object.freeze(dungeon.inventory); 39 | Object.freeze(dungeon.rooms); 40 | Object.freeze(dungeon); 41 | }); 42 | 43 | it('should update the console for a text action', () => { 44 | let newState = mainReducer(dungeon, { 45 | type: ACTION_TEXT, 46 | text: 'test' 47 | } as ITextAction); 48 | expect(newState.rooms).toEqual(dungeon.rooms); 49 | expect(newState.console.length).toBe(1); 50 | expect(newState.console[0]).toEqual('test'); 51 | }); 52 | 53 | it('should update the current room on a move', () => { 54 | let newState = mainReducer(dungeon, { 55 | type: ACTION_MOVE, 56 | direction: Directions.East, 57 | newRoom: room2 58 | } as IRoomAction); 59 | 60 | expect(newState.currentRoom.idx).toEqual(room2.idx); 61 | expect(newState.currentRoom.visited).toBe(true); 62 | }); 63 | 64 | it('should transfer inventory on a get', () => { 65 | let newState = mainReducer(dungeon, { 66 | type: ACTION_GET, 67 | item: thing1, 68 | room: room2 69 | } as IInventoryAction); 70 | expect(newState.inventory).toEqual([thing1]); 71 | expect(newState.rooms[1].things).toEqual([]); 72 | }); 73 | 74 | it('should transfer inventory and set the won flag on a win', () => { 75 | let newState = mainReducer(dungeon, { 76 | type: ACTION_WON, 77 | item: thing1, 78 | room: room2 79 | } as IWonAction); 80 | expect(newState.inventory).toEqual([thing1]); 81 | expect(newState.rooms[1].things).toEqual([]); 82 | expect(newState.won).toBe(true); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /src/app/actions/createAction.ts: -------------------------------------------------------------------------------- 1 | import { Dungeon } from '../world/dungeon'; 2 | import { Room } from '../world/room'; 3 | import { Thing } from '../world/thing'; 4 | import { Directions } from '../world/directions'; 5 | import { Action } from 'redux'; 6 | 7 | import { 8 | ACTION_E, 9 | ACTION_EAST, 10 | ACTION_G, 11 | ACTION_GET, 12 | ACTION_I, 13 | ACTION_INVENTORY, 14 | ACTION_L, 15 | ACTION_LOOK, 16 | ACTION_N, 17 | ACTION_NORTH, 18 | ACTION_S, 19 | ACTION_SOUTH, 20 | ACTION_W, 21 | ACTION_WEST, 22 | ACTION_WON, 23 | ACTION_TEXT, 24 | ACTION_MOVE 25 | } from './ActionList'; 26 | 27 | export interface ITextAction extends Action { 28 | type: string; 29 | text: string; 30 | } 31 | 32 | export interface IRoomAction extends Action { 33 | type: string; 34 | direction: Directions; 35 | newRoom: Room; 36 | } 37 | 38 | export interface IInventoryAction extends Action { 39 | type: string; 40 | item: Thing; 41 | room: Room; 42 | } 43 | 44 | export interface IWonAction extends IInventoryAction { } 45 | 46 | export interface IActionCreator { 47 | (dungeon: Dungeon, actionText: string): Action; 48 | } 49 | 50 | export const createAction: IActionCreator = (dungeon: Dungeon, actionText: string) => { 51 | 52 | let text = actionText.toLowerCase().trim(); 53 | 54 | if (text === ACTION_E || text === ACTION_EAST) { 55 | return checkDirection(dungeon, Directions.East); 56 | } 57 | if (text === ACTION_N || text === ACTION_NORTH) { 58 | return checkDirection(dungeon, Directions.North); 59 | } 60 | if (text === ACTION_W || text === ACTION_WEST) { 61 | return checkDirection(dungeon, Directions.West); 62 | } 63 | if (text === ACTION_S || text === ACTION_SOUTH) { 64 | return checkDirection(dungeon, Directions.South); 65 | } 66 | 67 | if (text === ACTION_L || text === ACTION_LOOK) { 68 | return { 69 | type: ACTION_TEXT, 70 | text: dungeon.currentRoom.longDescription 71 | } as ITextAction; 72 | } 73 | 74 | if (text === ACTION_I || text === ACTION_INVENTORY) { 75 | return checkInventory(dungeon); 76 | } 77 | 78 | if (text === ACTION_G || text === ACTION_GET) { 79 | return checkGet(dungeon); 80 | } 81 | 82 | return { 83 | type: ACTION_TEXT, 84 | text: 'What did you say?' 85 | }; 86 | } 87 | 88 | const checkDirection = (dungeon: Dungeon, dir: Directions) => { 89 | if (dungeon.currentRoom.directions[dir] === null) { 90 | return { 91 | type: ACTION_TEXT, 92 | text: 'You bump into the wall. OUCH!' 93 | } as Action; 94 | } 95 | return { 96 | type: ACTION_MOVE, 97 | direction: dir, 98 | newRoom: dungeon.currentRoom.directions[dir] 99 | } as IRoomAction; 100 | } 101 | 102 | const checkInventory = (dungeon: Dungeon) => { 103 | if (dungeon.inventory.length < 1) { 104 | return { 105 | type: ACTION_TEXT, 106 | text: 'You have nothing but the shirt on your back.' 107 | } as ITextAction; 108 | } 109 | return { 110 | type: ACTION_TEXT, 111 | text: 'You are carrying ' + dungeon.inventory.map(i => i.name).join(', ') + '.' 112 | } as ITextAction; 113 | } 114 | 115 | const checkGet: (dungeon: Dungeon) => Action = (dungeon: Dungeon) => { 116 | if (dungeon.currentRoom.things.length < 1) { 117 | return { 118 | type: ACTION_TEXT, 119 | text: 'You get down.' 120 | } as ITextAction; 121 | } 122 | 123 | let invCount = dungeon.inventory.length + 1; 124 | if (dungeon.trophyCount === invCount) { 125 | return { 126 | type: ACTION_WON, 127 | item: dungeon.currentRoom.things[0], 128 | room: dungeon.currentRoom 129 | } as IWonAction; 130 | } 131 | return { 132 | type: ACTION_GET, 133 | item: dungeon.currentRoom.things[0], 134 | room: dungeon.currentRoom 135 | } as IInventoryAction; 136 | } 137 | -------------------------------------------------------------------------------- /src/app/world/dungeonMaster.ts: -------------------------------------------------------------------------------- 1 | import { Dungeon } from './dungeon'; 2 | import { Room } from './room'; 3 | import { Directions, INVERSION_MAP } from './directions'; 4 | import { GeneratorSeed } from '../seed/generatorSeed'; 5 | import { THING_SEED } from '../seed/thingSeed'; 6 | import { 7 | GRID_SIZE, 8 | CELLS 9 | } from '../settings'; 10 | 11 | function extractRandom(list: T[]): T { 12 | return list[Math.floor(Math.random() * list.length)]; 13 | } 14 | 15 | const DIRECTION_MAP: number[] = [-1 * GRID_SIZE, GRID_SIZE, 1, -1]; 16 | 17 | let generateRooms = (dungeon: Dungeon) => { 18 | 19 | let seed: GeneratorSeed = new GeneratorSeed(); 20 | 21 | for (let x = 0; x < CELLS; x += 1) { 22 | let room = new Room(); 23 | room.idx = x; 24 | let description = extractRandom(seed.rooms).description; 25 | let wall = extractRandom(seed.walls).description; 26 | let feature = extractRandom(seed.features).description; 27 | room.name = 'A ' + description + ' room'; 28 | room.description = 'You are standing inside a ' + 29 | description + ' room. You are surrounded by ' + wall 30 | + '. ' + feature; 31 | dungeon.rooms.push(room); 32 | } 33 | 34 | return dungeon; 35 | }; 36 | 37 | let assign = (dungeon: Dungeon, idx: number, dir: Directions) => { 38 | let room = dungeon.rooms[idx]; 39 | let otherRoom = dungeon.rooms[idx + DIRECTION_MAP[dir]]; 40 | room.setDirection(dir, otherRoom); 41 | otherRoom.setDirection(INVERSION_MAP[dir], room); 42 | return dungeon; 43 | }; 44 | 45 | let connectRooms = (dungeon: Dungeon) => { 46 | 47 | let gridLessOne = GRID_SIZE - 1; 48 | 49 | // first put walls around the perimeter and connect all internal rooms 50 | // (the connection is converse, so connecting 1 -> 2 connects 2 -> 1) 51 | for (let northToSouth = 0; northToSouth < GRID_SIZE; northToSouth += 1) { 52 | for (let westToEast = 0; westToEast < GRID_SIZE; westToEast += 1) { 53 | let cell = northToSouth * GRID_SIZE + westToEast; 54 | let room = dungeon.rooms[cell]; 55 | if (northToSouth === 0) { 56 | room.walls.push(Directions.North); 57 | } 58 | if (northToSouth === gridLessOne) { 59 | room.walls.push(Directions.South); 60 | } 61 | if (westToEast === 0) { 62 | room.walls.push(Directions.West); 63 | } 64 | if (westToEast === gridLessOne) { 65 | room.walls.push(Directions.East); 66 | } 67 | if (northToSouth < gridLessOne) { 68 | dungeon = assign(dungeon, cell, Directions.South); 69 | } 70 | if (westToEast < gridLessOne) { 71 | dungeon = assign(dungeon, cell, Directions.East); 72 | } 73 | } 74 | } 75 | 76 | for (let northToSouth = 1; northToSouth < gridLessOne; northToSouth += 1) { 77 | for (let westToEast = 1; westToEast < gridLessOne; westToEast += 1) { 78 | let cell = northToSouth * GRID_SIZE + westToEast; 79 | let room = dungeon.rooms[cell]; 80 | if (room.walls.length > 0) { 81 | continue; 82 | } 83 | let options: {dir: Directions, room: Room}[] = []; 84 | for (let idx = 0; idx < room.directions.length; idx += 1) { 85 | if (room.directions[idx] !== null) { 86 | options.push({dir: idx, room: room.directions[idx]}); 87 | } 88 | } 89 | let wall = options[Math.floor(Math.random() * options.length)]; 90 | if (wall.room.walls.length > 0) { 91 | continue; 92 | } 93 | // this room loses the reference and gains a wall 94 | room.setDirection(wall.dir, null); 95 | room.walls.push(wall.dir); 96 | // other room loses the reference and gains a wall 97 | wall.room.setDirection(INVERSION_MAP[wall.dir], null); 98 | wall.room.walls.push(INVERSION_MAP[wall.dir]); 99 | } 100 | } 101 | 102 | return dungeon; 103 | }; 104 | 105 | let placeArtifacts = (dungeon: Dungeon) => { 106 | dungeon.trophyCount = THING_SEED.length; 107 | for (let idx = 0; idx < THING_SEED.length; idx += 1) { 108 | let roomIdx = Math.floor(Math.random() * CELLS); 109 | let room = dungeon.rooms[roomIdx]; 110 | room.things.push(THING_SEED[idx]); 111 | } 112 | return dungeon; 113 | }; 114 | 115 | export const DUNGEON_MASTER: () => Dungeon = () => { 116 | 117 | let dungeon: Dungeon = new Dungeon(); 118 | 119 | dungeon = generateRooms(dungeon); 120 | dungeon = connectRooms(dungeon); 121 | dungeon = placeArtifacts(dungeon); 122 | dungeon.currentRoomIdx = Math.floor(Math.random() * CELLS); 123 | dungeon.currentRoom.visited = true; 124 | dungeon.console.push(dungeon.currentRoom.longDescription); 125 | 126 | return dungeon; 127 | }; 128 | --------------------------------------------------------------------------------