Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id='app'></div>
              
            
!

CSS

              
                body, html {
  margin: 0 auto;
  width: 90%;
  text-align: center;
}
.gameinfo {
  padding: 0;
  li {
    display: inline;
    list-style: none;
    margin: 5px;
  }
}
.gamefield {
  clear: left;
}
.tile {
  width: 20px;
  height: 20px;
  display: inline-block;
  overflow: hidden;
  // border: 1px solid beige;
  &.floor {
    background-color: gray;
  }
  &.wall {
    background-color: #555;
  }
  &.void {
    background-color: black;
  }
  &.player {
    background-color: blue;
  }
  &.weapon {
    background-color: orange;
  }
  &.health {
    background-color: green;
  }
  &.shade {
    background-color: hsla(217º, 59%, 14%, 50%);
  }
}
.row {
  text-align: center;
  font-size: 0;
}
              
            
!

JS

              
                const GameBoard = () => {
  return(
    <div>
      <h1>Dungeon Game</h1>
      <p>Kill all the enemies to win!</p>
      <Dashboard />
      <VisibleMap />
    </div>
  );
};
const DashboardStats = ({ stats, onToggleClick }) => {
  return (
    <ul className='gameinfo'>
      <li><b>Health: </b>{stats.health.toFixed(2)}</li>
      <li><b>XP: </b>{stats.xp.toFixed(2)}</li>
      <li><b>Damage: </b>{stats.damage.toFixed(2)}</li>
      <li><button onClick={() => onToggleClick()}>Toggle darkness</button></li>
    </ul>
  );
};
const Board = ({ board }) => {
  return (
    <div className='gamefield'>
      {board.map((rowArr, i) => <Row key={i} row={rowArr} />)}
    </div>
  );
};
class Row extends React.Component {
  shouldComponentUpdate(nextProps) {
    //console.log(!Immutable.is(this.props.row, nextProps.row));
    return !Immutable.is(this.props.row, nextProps.row);
  }
  render(){
    return (
      <div className='row'>
        {this.props.row.map((columnVal, i) => <Tile columnVal={columnVal} key={i} />)}
      </div>
    ); 
  }
};
class Tile extends React.Component {
  shouldComponentUpdate(nextProps) {
    //console.log(this.props.columnVal !== nextProps.columnVal);
    return this.props.columnVal !== nextProps.columnVal;
  }
  enemyTileStyle = (eff_d) => ({
    backgroundColor: 'hsla(0, 100%, 50%, 0.5)',
    boxSizing: 'border-box',
    border: '10px solid hsla(360, ' + (eff_d % 1).toFixed(4) * 100 + '%' + ', 50%, 0.5)'
  });
  render(){
    return (
      <span className={(() => {
          switch (Math.floor(this.props.columnVal)){
            case 0:
              return 'tile void';
            case 1:
              return 'tile floor';
            case 2:
              return 'tile wall';
            case 3:
              return 'tile player';
              // case 4:
              //   return 'tile enemy';
            case 5:
              return 'tile weapon';
            case 6:
              return 'tile health';
            case 7:
              return 'tile shade';
            default:
              return 'tile';
                                             }
        })()} style={
          Math.floor(this.props.columnVal) === 4 ? this.enemyTileStyle(this.props.columnVal) : {}
        } ></span>
    )
  }
};


class DungeonUtils {
  constructor(immutableDungeonObj){
    this._map = immutableDungeonObj.map.toJS();
    this._mapSize = immutableDungeonObj.mapSize;
    this._player = immutableDungeonObj.player.toJS(),
    this._enemies = immutableDungeonObj.enemies.toJS(),
    this._healthTiles = immutableDungeonObj.healthTiles.toJS(),
    this._weaponTiles = immutableDungeonObj.weaponTiles.toJS(),
    this._gameFlag = immutableDungeonObj.gameFlag
  }

  get gameFlag(){
    return this._gameFlag;
  }
  get dungeonSpecs(){
    return {
      map: Immutable.fromJS(this._map),
      mapSize: this._mapSize,
      player: Immutable.fromJS(this._player),
      enemies: Immutable.fromJS(this._enemies),
      healthTiles: Immutable.fromJS(this._healthTiles),
      weaponTiles: Immutable.fromJS(this._weaponTiles),
      gameFlag: this._gameFlag
    }
  }
  generate () {
    for (let x = 0; x < this._mapSize; x++) {
      this._map[x] = [];
      for (let y = 0; y < this._mapSize; y++) {
        this._map[x][y] = 0;
      }
    }

    let rooms = [];
    let room_count = this.getRandom(10, 20);
    let min_size = 5;
    let max_size = 15;

    for (let i = 0; i < room_count; i++) {
      let room = {};

      room.x = this.getRandom(1, this._mapSize - max_size - 1);
      room.y = this.getRandom(1, this._mapSize - max_size - 1);
      room.w = this.getRandom(min_size, max_size);
      room.h = this.getRandom(min_size, max_size);

      if (this.doesCollide(room, rooms)) {
        i--;
        continue;
      }
      room.w--;
      room.h--;
      rooms.push(room)
    }
    this.squashRooms(rooms);

    for (let i = 0; i < room_count; i++) {
      let roomA = rooms[i];
      let roomB = this.findClosestRoom(roomA, rooms);

      let pointA = {
        x: this.getRandom(roomA.x, roomA.x + roomA.w),
        y: this.getRandom(roomA.y, roomA.y + roomA.h)
      };
      let pointB = {
        x: this.getRandom(roomB.x, roomB.x + roomB.w),
        y: this.getRandom(roomB.y, roomB.y + roomB.h)
      };

      while ((pointB.x != pointA.x) || (pointB.y != pointA.y)) {
        if (pointB.x != pointA.x) {
          if (pointB.x > pointA.x) pointB.x--;
          else pointB.x++;
        } else if (pointB.y != pointA.y) {
          if (pointB.y > pointA.y) pointB.y--;
          else pointB.y++;
        }
        this._map[pointB.x][pointB.y] = 1;
      }
    }

    for (let i = 0; i < room_count; i++) {
      let room = rooms[i];
      for (let x = room.x; x < room.x + room.w; x++) {
        for (let y = room.y; y < room.y + room.h; y++) {
          this._map[x][y] = 1;
        }
      }
    }

    for (let x = 0; x < this._mapSize; x++) {
      for (let y = 0; y < this._mapSize; y++) {
        if (this._map[x][y] == 1) {
          for (var xx = x - 1; xx <= x + 1; xx++) {
            for (var yy = y - 1; yy <= y + 1; yy++) {
              if (this._map[xx][yy] == 0) this._map[xx][yy] = 2;
            }
          }
        }
      }
    }
    this.generateItemStats();
    this.placeTiles('enemy');
    this.placeTiles('health');
    this.placeTiles('weapon');
    this.placeTiles('player');
    this.augmentMap();
  }
  findClosestRoom (room, rooms) {
    let mid = {
      x: room.x + (room.w / 2),
      y: room.y + (room.h / 2)
    };
    let closest = null;
    let closest_distance = 1000;
    for (let i = 0; i < rooms.length; i++) {
      let check = rooms[i];
      if (check == room) continue;
      let check_mid = {
        x: check.x + (check.w / 2),
        y: check.y + (check.h / 2)
      };
      let distance = Math.min(Math.abs(mid.x - check_mid.x) - (room.w / 2) - (check.w / 2), Math.abs(mid.y - check_mid.y) - (room.h / 2) - (check.h / 2));
      if (distance < closest_distance) {
        closest_distance = distance;
        closest = check;
      }
    }
    return closest;
  }
  
  squashRooms (rooms) {
    for (let i = 0; i < 10; i++) {
      for (let j = 0; j < rooms.length; j++) {
        let room = rooms[j];
        while (true) {
          let old_position = {
            x: room.x,
            y: room.y
          };
          if (room.x > 1) room.x--;
          if (room.y > 1) room.y--;
          if ((room.x == 1) && (room.y == 1)) break;
          if (this.doesCollide(room, rooms, j)) {
            room.x = old_position.x;
            room.y = old_position.y;
            break;
          }
        }
      }
    }
  }
  
  doesCollide (room, rooms, ignore) {
    for (let i = 0; i < rooms.length; i++) {
      if (i == ignore) continue;
      let check = rooms[i];
      if (!((room.x + room.w < check.x) || (room.x > check.x + check.w) || (room.y + room.h < check.y) || (room.y > check.y + check.h))) return true;
    }
    return false;
  }
  
  placeTiles(tileType) {
    let emptyTilesArr= this._map.map( (row, rowIndex) => { 
      let indexes = [], i;
      for(i = 0; i < row.length; i++)
        if (row[i] === 1)
          indexes.push({x: rowIndex, y: i});
      return indexes;
    });
    //Flatten the empty tiles array
    emptyTilesArr = [].concat.apply([], emptyTilesArr);
    
    let tileCount;
    switch(tileType){
      case 'player':
        tileCount = 1;
        break;
      case 'enemy':
        tileCount = this._enemies.length;
        break;
      case 'weapon':
        tileCount = this._weaponTiles.length;
        break;
      case 'health':
        tileCount = this._healthTiles.length;
        break;
    }
    let coorIdx;
    for (let i = 0; i < tileCount; i++){
      coorIdx = this.getRandom(0, emptyTilesArr.length - 1);
      switch (tileType){
        case 'player':
          this._map[emptyTilesArr[coorIdx].x][emptyTilesArr[coorIdx].y] = 3;
          this._player.pos = {'x': emptyTilesArr[coorIdx].x, 'y': emptyTilesArr[coorIdx].y};
          break;
        case 'enemy':
          this._map[emptyTilesArr[coorIdx].x][emptyTilesArr[coorIdx].y] = 4;
          this._enemies[i].pos = {'x': emptyTilesArr[coorIdx].x, 'y': emptyTilesArr[coorIdx].y};
          break;
        case 'weapon':
          this._map[emptyTilesArr[coorIdx].x][emptyTilesArr[coorIdx].y] = 5;
          this._weaponTiles[i].pos = {'x': emptyTilesArr[coorIdx].x, 'y': emptyTilesArr[coorIdx].y};
          break;
        case 'health':
          this._map[emptyTilesArr[coorIdx].x][emptyTilesArr[coorIdx].y] = 6;
          this._healthTiles[i].pos = {'x': emptyTilesArr[coorIdx].x, 'y': emptyTilesArr[coorIdx].y};
          break;
      }
      emptyTilesArr.splice(coorIdx, 1);
    }
  }
  generateItemStats() {
    this._player.health = getRandomDecimal(0.5, 20);
    this._player.xp = getRandomDecimal(1, 5);
    let minEnemyCount = this.getRandom(5, 10);
    for (let i = 0; i < minEnemyCount; i++){
       createEnemy(this);
    }
    while (maxEnemyHealth(this) > playerMaxEffectiveDamage(this)){
      createWeaponTile(this);
    }
    while (maxPlayerHealth(this) < sumOfEnemiesEffectiveDamage(this)){
      createHealthTile(this);
    }
    function maxEnemyHealth (context) {
      return context._enemies.length === 0 ? 0 : Math.max.apply(null, context._enemies.map(enemy => enemy.health));
    }
    function playerMaxEffectiveDamage(context) {
      //Hypothetically how experience should be calculated. a function of enemy XP and number of enemies
      return Math.pow(context._player.xp, 1/2) *
        context._weaponTiles.reduce ( (prev, curr) => prev + curr.damage, context._player.damage);
    }
    function maxPlayerHealth(context){
      return context._player.health + context._healthTiles.reduce( (prev, curr) => prev + curr.value, 0 );
    }
    function sumOfEnemiesEffectiveDamage(context) {
      return context._enemies.reduce((prev, curr) => prev + curr.xp * curr.damage, 0);
    }
    function createEnemy(context){
      let enemy = {};
      enemy.health = getRandomDecimal(10, 30);
      enemy.xp = getRandomDecimal(1, 3);
      enemy.damage = getRandomDecimal(0.1, 3);
      context._enemies.push(enemy);
    }
    function createWeaponTile(context){
      let weapon = {};
      weapon.damage = getRandomDecimal(0.1, 3);
      context._weaponTiles.push(weapon);
    }
    function createHealthTile(context){
      let health = {};
      health.value = getRandomDecimal(1, 10);
      context._healthTiles.push(health);
    }
    function getRandomDecimal(low, high){
      //Both inclusive
      return (Math.random() * (high - low + 1)) + low;
    }
  }
  getRandom (low, high) {
    //High is exclusive
    return~~ (Math.random() * (high - low)) + low;
  }

  augmentMap() {
    let maxPower = Math.max(...this._enemies.map((enemy) => enemy.xp * enemy.damage));
    let minPower = Math.min(...this._enemies.map((enemy) => enemy.xp * enemy.damage));
    let scaleMax = 0.99, scaleMin = 0.01;
    let enemiesScaledPower = 
        this._enemies.map((enemy) => ({'pos': enemy.pos, 'power': ((scaleMax - scaleMin)*(enemy.xp * enemy.damage - minPower ) / (maxPower - minPower)) + scaleMin }));
    this._map = 
      this._map.map((rowArr, x) => rowArr.map((val, y) => val === 4 ? 4 + enemiesScaledPower.filter((enemyScaledPower) => enemyScaledPower.pos.x === x && enemyScaledPower.pos.y === y)[0].power : val ));
  }
}

function attack(dungeonObj, enemyIdx){
  //returns true if player survives and false otherwise
  //player hits first
  let enemy = dungeonObj.enemies.get(enemyIdx);
  //let attackCoef = this.player.xp * this.player.damage / enemy.health;
  let enemyHealthAfterHit = enemy.get('health') - dungeonObj.getIn(['player','xp']) * dungeonObj.getIn(['player','damage']);
  let playerHealthAfterHit = dungeonObj.getIn(['player','health']) - enemy.get('xp') * enemy.get('damage');
  if (playerHealthAfterHit <= 0 ) return false;
  else if (enemyHealthAfterHit <= 0) {
    dungeonObj = dungeonObj.set('enemies', dungeonObj.enemies.delete(enemyIdx));
    dungeonObj = dungeonObj.set('map', dungeonObj.map.setIn([enemy.getIn(['pos','x']), enemy.getIn(['pos','y'])], 1));
    dungeonObj = dungeonObj.set('player', dungeonObj.player.set('health', playerHealthAfterHit));
    dungeonObj = dungeonObj.set('player', dungeonObj.player.set('xp', dungeonObj.player.get('xp') + Math.pow(enemy.get('xp'), 1/2)));
  } else {
    dungeonObj = dungeonObj.set('enemies', dungeonObj.enemies.setIn([enemyIdx, 'health'], enemyHealthAfterHit));
    dungeonObj = dungeonObj.set('player', dungeonObj.player.set('health', playerHealthAfterHit));
  }
  return dungeonObj;
}
function movePlayer (dungeonObj, direction) {
  //let newDungeonObj;
  let newPos = dungeonObj.player.get('pos');
  switch (direction){
    case 'ArrowUp':
      newPos = newPos.update('x', x => x - 1);
      break;
    case 'ArrowDown':
      newPos = newPos.update('x', x => x + 1);
      break;
    case 'ArrowLeft':
      newPos = newPos.update('y', y => y - 1);
      break;
    case 'ArrowRight':
      newPos = newPos.update('y', y => y + 1);
      break;
  }
  let nextTile = dungeonObj.map.getIn([newPos.get('x'), newPos.get('y')]);
  if ([0, 2, undefined].indexOf(nextTile) == -1 ) {
    switch (Math.floor(nextTile)){
      case 4: 
        
        let enemyIdx = dungeonObj.enemies.findIndex((enemy) => enemy.getIn(['pos','x']) === newPos.get('x') && enemy.getIn(['pos','y']) === newPos.get('y'));
        let enemy = dungeonObj.enemies.get(enemyIdx);

        let attackResult = attack(dungeonObj, enemyIdx);
        if (attackResult) dungeonObj = attackResult;
        if (!attackResult) { 
          //Game OVer
          var notifier = humane.create({baseCls: 'humane-original', timeout: 1000});
          notifier.error = notifier.spawn({addnCls: 'humane-original-error'});
          notifier.error('You Lost');
          dungeonObj = dungeonObj.set('gameFlag', -1);
          break; 
        } else if (dungeonObj.map.getIn([enemy.getIn(['pos','x']), enemy.getIn(['pos','y'])]) === 1){
          //Enemy defeated
          dungeonObj = dungeonObj.set('map', dungeonObj.map.setIn([dungeonObj.getIn(['player','pos','x']), dungeonObj.getIn(['player','pos','y'])], 1));
          dungeonObj = dungeonObj.set('map', dungeonObj.map.setIn([newPos.get('x'), newPos.get('y')], 3));
          dungeonObj = dungeonObj.set('player', dungeonObj.player.set('pos', newPos));
          if (dungeonObj.enemies.size === 0){
            //Game Won
            var notifier = humane.create({baseCls: 'humane-original', timeout: 1000});
            notifier.success = notifier.spawn({addnCls: 'humane-original-success'});
            notifier.success('You Won');
            dungeonObj = dungeonObj.set('gameFlag', 1); 
          }
        }
        break;
      case 5:
        let weaponIdx = dungeonObj.weaponTiles.findIndex((weapon) => weapon.getIn(['pos','x']) === newPos.get('x') && weapon.getIn(['pos','y']) === newPos.get('y'));
        let weapon = dungeonObj.weaponTiles.get(weaponIdx);
        dungeonObj = dungeonObj.set('player', dungeonObj.player.set('damage', dungeonObj.player.get('damage') + weapon.get('damage')));
        dungeonObj = dungeonObj.set('weaponTiles', dungeonObj.weaponTiles.delete(weaponIdx));
        dungeonObj = dungeonObj.set('map', dungeonObj.map.setIn([dungeonObj.getIn(['player','pos','x']), dungeonObj.getIn(['player','pos','y'])], 1));
        dungeonObj = dungeonObj.set('map', dungeonObj.map.setIn([newPos.get('x'), newPos.get('y')], 3));
        dungeonObj = dungeonObj.set('player', dungeonObj.player.set('pos', newPos));
        break;
      case 6:
        let healthIdx = dungeonObj.healthTiles.findIndex((health) => health.getIn(['pos','x']) === newPos.get('x') && health.getIn(['pos','y']) === newPos.get('y'));
        let health = dungeonObj.healthTiles.get(healthIdx);
        dungeonObj = dungeonObj.set('player', dungeonObj.player.set('health', dungeonObj.player.get('health') + health.get('value')));
        dungeonObj = dungeonObj.set('healthTiles', dungeonObj.healthTiles.delete(healthIdx));
        dungeonObj = dungeonObj.set('map', dungeonObj.map.setIn([dungeonObj.getIn(['player','pos','x']), dungeonObj.getIn(['player','pos','y'])], 1));
        dungeonObj = dungeonObj.set('map', dungeonObj.map.setIn([newPos.get('x'), newPos.get('y')], 3));
        dungeonObj = dungeonObj.set('player', dungeonObj.player.set('pos', newPos));
        break;
      default:
        dungeonObj = dungeonObj.set('map', dungeonObj.map.setIn([dungeonObj.getIn(['player','pos','x']), dungeonObj.getIn(['player','pos','y'])], 1));
        dungeonObj = dungeonObj.set('map', dungeonObj.map.setIn([newPos.get('x'), newPos.get('y')], 3));
        dungeonObj = dungeonObj.set('player', dungeonObj.player.set('pos', newPos));
    }
    return dungeonObj;
  } else 
    return false;
}

const DungeonObj = Immutable.Record({
  map : Immutable.List([]),
  mapSize : 64,
  player : Immutable.Map({ 'pos': null, 'health': null, 'xp': 1, 'damage': 1 }),
  enemies : Immutable.List([]),
  healthTiles : Immutable.List([]),
  weaponTiles : Immutable.List([]),
  gameFlag : 0,
});

//*****************************Action Creators*****************************//
function gridResize(windowWidth, windowHeight) {
  let numOfTilesH = Math.floor(((70/100) * windowHeight) / 20);
  let numOfTilesW = Math.floor(((70/100) * windowWidth) / 20);

  return {
    type: 'GRID_RESIZE',
    newBoardWidth: numOfTilesW,
    newBoardHeight: numOfTilesH
  };
}
function keyPress(key){

  return {
    type: 'MOVE',
    direction: key
  }
}
function toggleShadow() {
  return {
    type: 'TOGGLE_SHADOW'
  }
}

//*****************************Reducers*****************************//
const gridReducer = (state, action) => { 
  if (typeof state === 'undefined'){
    let rawDungeon = new DungeonObj();
    let dungeonUtils = new DungeonUtils(rawDungeon);
    dungeonUtils.generate();
    let newDungeon = new DungeonObj(dungeonUtils.dungeonSpecs);
    return Immutable.Map({
            boardWidth: Math.floor(((70/100) * window.innerWidth) / 20),
            boardHeight: Math.floor(((70/100) * window.innerHeight) / 20),
            shadow: true
           }).concat(newDungeon);
  }
  switch (action.type) {
    case 'GRID_RESIZE': 
      return state.merge(Immutable.Map({ 
              boardWidth: action.newBoardWidth,
              boardHeight: action.newBoardHeight
      }));
    case 'MOVE':
      let oldDungeon = new DungeonObj(state);
      //let dungeonUtils = new DungeonUtils(oldDungeon);
      let newDungeon;
      let moveResult = movePlayer(oldDungeon, action.direction);
      if (moveResult && moveResult.get('gameFlag') === 0) {
        newDungeon = moveResult;
      } else if (moveResult && moveResult.get('gameFlag') !== 0) {
        let rawDungeon = new DungeonObj();
        let dungeonUtils = new DungeonUtils(rawDungeon);
        dungeonUtils.generate();
        newDungeon = new DungeonObj(dungeonUtils.dungeonSpecs);
      } else {
        newDungeon = oldDungeon;
      }
      return state.merge(newDungeon);
    case 'TOGGLE_SHADOW':
      return state.merge(Immutable.Map({
        shadow: !state.get('shadow')
      }));
    default:
      return state;
  }
}
const dashboardReducer = (state, action) => {
  if (typeof state === 'undefined'){
    return {}; // This reducer is only for the toggle button
  }
  switch (action.type){
    default:
      return state;
  }
}

const reducers = Immutable.Map({
  'gridReducer': gridReducer,
  'dashboardReducer': dashboardReducer
})

const gameReducer = function(state = Immutable.Map({}), action) {
  reducers.forEach((reducer, key) => {
    state = state.set(key, reducer(state.get(key), action))
  });
  return state;
}

//*****************************Redux Store*****************************//

let store = Redux.createStore(gameReducer, window.devToolsExtension && window.devToolsExtension());

window.addEventListener('resize', () => {
  store.dispatch(gridResize(window.innerWidth, window.innerHeight));
});

window.addEventListener('keydown', (e) => {
  store.dispatch(keyPress(e.key));
});

const getGridMap = Reselect.createSelector(
  (state) => state.getIn(['gridReducer', 'map']),
  (map) => map
);

const getReducedMap = Reselect.createSelector(
  getGridMap,
  (state) => state.get('gridReducer'),
  (gridMap, grid) => {
    let gridMax = grid.get('mapSize') - 1;
    let boardH = grid.get('boardHeight');
    let boardW = grid.get('boardWidth');   
    let playerPosX = grid.getIn(['player', 'pos', 'x']);
    let playerPosY = grid.getIn(['player', 'pos', 'y']);
    let half_x = ~~(boardH/2);
    let half_y = ~~(boardW/2);
    let x_min_offset = playerPosX - half_x < 0 ? - (playerPosX - half_x) : 0;
    let x_max_offset = playerPosX + half_x > gridMax ? playerPosX + half_x - gridMax : 0; 
    let y_min_offset = playerPosY - half_y < 0 ? - (playerPosY - half_y) : 0;
    let y_max_offset = playerPosY + half_y > gridMax ? playerPosY + half_y - gridMax : 0;
    
    let x_min = playerPosX - half_x >= 0 ? playerPosX - half_x - x_max_offset : 0;
    let x_max = playerPosX + half_x <= gridMax ? playerPosX + half_x + x_min_offset : gridMax;
    let y_min = playerPosY - half_y >= 0 ? playerPosY - half_y - y_max_offset : 0;
    let y_max = playerPosY + half_y <= gridMax ? playerPosY + half_y + y_min_offset : gridMax;
    let reducedMap = gridMap.toSeq().slice(x_min, x_max).map((row) => row.slice(y_min, y_max)); 
    let shadow = grid.get('shadow');
    let shadowedMap = reducedMap;
    if (shadow){
      let reducedViewPlayerPos = Immutable.Map(); 
      reducedViewPlayerPos = reducedViewPlayerPos.set('x', reducedMap.findIndex((mapRow, x) => mapRow.indexOf(3) !== -1));
      reducedViewPlayerPos = reducedViewPlayerPos.set('y', reducedMap.get(reducedViewPlayerPos.get('x') + '').indexOf(3));
      shadowedMap = reducedMap.map((mapRow, x) => mapRow.map((mapTile, y) => Math.abs(reducedViewPlayerPos.get('x') - x) <= 3 && Math.abs(reducedViewPlayerPos.get('y') - y) <= 3 ? mapTile : 7 ));
    }
    return shadowedMap;
  }
);

const mapStateToVisibleMapProps = (state) => {
  return {
    board : getReducedMap(state)
  };
}

const VisibleMap = ReactRedux.connect(
  mapStateToVisibleMapProps
)(Board);


const getPlayer = (state) => state.getIn(['gridReducer', 'player']);
const getStats = Reselect.createSelector([getPlayer], (player) => (
  {
   'health': player.get('health'),
   'xp':player.get('xp'),
   'damage':player.get('damage')
  }
));

const mapStateToDashboardProps = (state) => {
  return {
    stats: getStats(state)
  }
}
const mapDispatchToDashboardProps = (dispatch) => {
  return {
    onToggleClick: () => {
      dispatch(toggleShadow())
    }
  }
}

const Dashboard = ReactRedux.connect(
  mapStateToDashboardProps,
  mapDispatchToDashboardProps
)(DashboardStats);

ReactDOM.render(
  <ReactRedux.Provider store={store}>
    <GameBoard />
  </ReactRedux.Provider>, 
  document.getElementById('app')
);

//TODO: 
//Watch idiomatic Redux egghead
//use PropTypes
//map has dead ends
//does it make sense to have playerPos at all?
//change context parameters to bind
//read dont make inner functions except if you have too article for generateGameStats
//change enemy generation algorithm
//can we use lodash? underscore
//use instead https://goo.gl/FQiZ0u
              
            
!
999px

Console