├── 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 |
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 |
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 | 
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 |
--------------------------------------------------------------------------------