Browse Source

Merged in feature/TH-21-first-game-step-deck-selection (pull request #7)

Feature/TH-21 first game step deck selection
Johan LE BAUT 4 years ago
parent
commit
050469fea4
45 changed files with 2802 additions and 591 deletions
  1. 2 0
      .env.development
  2. 0 39
      .vscode/settings.json
  3. 49 0
      project.code-workspace
  4. 9 2
      server/.babelrc
  5. 1 0
      server/.gitignore
  6. 46 0
      server/.vscode/launch.json
  7. 0 97
      server/game-server/online-duel-sync.js
  8. 868 17
      server/package-lock.json
  9. 6 2
      server/package.json
  10. 1 1
      server/src/client-server-shared/all-heroes.json
  11. 156 0
      server/src/client-server-shared/duel-controller.js
  12. 55 0
      server/src/client-server-shared/gameStates/0-initState.js
  13. 89 0
      server/src/client-server-shared/gameStates/1-selectDraftState.js
  14. 56 0
      server/src/client-server-shared/gameStates/1-selectFactionState.js
  15. 44 0
      server/src/client-server-shared/gameStates/1-selectTournamentState.js
  16. 41 0
      server/src/client-server-shared/gameStates/2-changeUpTo3Cards.js
  17. 22 0
      server/src/client-server-shared/gameStates/gameStates.js
  18. 85 0
      server/src/client-server-shared/heroesHelper.js
  19. 41 10
      server/src/db/mariadb-connector.js
  20. 35 4
      server/src/game-server/games-manager.js
  21. 191 0
      server/src/game-server/online-duel-sync.js
  22. 0 0
      server/src/players/player-id.js
  23. 23 26
      server/src/server.js
  24. 12 12
      server/src/tools/server-tool-listener.js
  25. 15 16
      server/src/tools/server-tools.js
  26. 75 24
      src/App.vue
  27. 97 11
      src/common/heroes-display/HeroesDisplay.vue
  28. 1 0
      src/common/heroes-display/components/HeroesFilter.vue
  29. 42 2
      src/common/heroes-display/components/HeroesSelector.vue
  30. 19 3
      src/common/socket-service.js
  31. 38 64
      src/game/AppGame.vue
  32. 20 0
      src/game/components/GameBoard.vue
  33. 13 0
      src/game/components/OnlineWait.vue
  34. 3 0
      src/main.js
  35. 12 1
      src/menu/AppMenu.vue
  36. 19 34
      src/menu/game-creation/MenuGameCreation.vue
  37. 52 66
      src/menu/login/MenuLogin.vue
  38. 33 45
      src/menu/online-room/MenuOnlineRoom.vue
  39. 0 28
      src/server-shared/heroesHelper.js
  40. 182 0
      src/store/game/game.js
  41. 75 0
      src/store/game/player/any-player.js
  42. 0 85
      src/store/heroes/heroes.js
  43. 188 0
      src/store/menu/menu.js
  44. 47 2
      src/store/store.js
  45. 39 0
      src/store/types.js

+ 2 - 0
.env.development

@@ -1,4 +1,6 @@
 BROWSER=firefox
+BABEL_ENV=debug
+NODE_ENV=development
 
 //    1. Game server
 

+ 0 - 39
.vscode/settings.json

@@ -1,39 +0,0 @@
-{
-    "javascript.implicitProjectConfig.checkJs": true,
-    "terminal.integrated.shell.windows": "C:\\WINDOWS\\System32\\wsl.exe",
-    "javascript.updateImportsOnFileMove.enabled": "always",
-    "typescript.validate.enable": false,
-    "eslint.alwaysShowStatus": true,
-    "eslint.workingDirectories": [
-        "./"
-    ],
-    "javascript.validate.enable": false,
-    "window.zoomLevel": 0,
-    "[javascript]": {
-        "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
-    },
-    "[vue]": {
-    
-        "editor.defaultFormatter": "octref.vetur"
-    },
-    "emmet.showAbbreviationSuggestions": true,
-    "emmet.showExpandedAbbreviation": "always",
-    "emmet.syntaxProfiles": {
-        "vue-html": "html",
-        "vue":"html"
-    },
-    "vetur.format.defaultFormatter.html": "js-beautify-html",
-    "vetur.format.defaultFormatter.js": "prettier-eslint",
-    "vue-format.format_need": [
-        "html",
-        "js",
-        "css"
-    ],
-    "editor.codeActionsOnSave": {
-        "source.fixAll.eslint": true
-    },
-    "editor.suggestSelection": "first",
-    "emmet.excludeLanguages": [
-        "markdown"
-    ]
-}

+ 49 - 0
project.code-workspace

@@ -0,0 +1,49 @@
+{
+	"folders": [
+		{
+			"path": "."
+		},
+		{
+			"path": "server"
+		}
+	],
+	"settings": {
+		"javascript.implicitProjectConfig.checkJs": true,
+		"terminal.integrated.shell.windows": "C:\\WINDOWS\\System32\\wsl.exe",
+		"javascript.updateImportsOnFileMove.enabled": "always",
+		"typescript.validate.enable": false,
+		"eslint.alwaysShowStatus": true,
+		"eslint.workingDirectories": [
+			"./"
+		],
+		"javascript.validate.enable": false,
+		"window.zoomLevel": -1,
+		"[javascript]": {
+			"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
+		},
+		"[vue]": {
+		
+			"editor.defaultFormatter": "octref.vetur"
+		},
+		"emmet.showAbbreviationSuggestions": true,
+		"emmet.showExpandedAbbreviation": "always",
+		"emmet.syntaxProfiles": {
+			"vue-html": "html",
+			"vue":"html"
+		},
+		"vetur.format.defaultFormatter.html": "js-beautify-html",
+		"vetur.format.defaultFormatter.js": "prettier-eslint",
+		"vue-format.format_need": [
+			"html",
+			"js",
+			"css"
+		],
+		"editor.codeActionsOnSave": {
+			"source.fixAll.eslint": true
+		},
+		"editor.suggestSelection": "first",
+		"emmet.excludeLanguages": [
+			"markdown"
+		]
+	}
+}

+ 9 - 2
server/.babelrc

@@ -1,7 +1,14 @@
 {
   "presets": ["@babel/preset-env"],
   "plugins": [
-    "@babel/plugin-proposal-object-rest-spread"
-  ]
+    "@babel/plugin-proposal-object-rest-spread",
+    "@babel/plugin-transform-runtime"
+  ],
+  "env": {
+    "debug": {
+      "sourceMaps": "inline",
+      "retainLines": true
+    }
+  }
 }
 

+ 1 - 0
server/.gitignore

@@ -66,6 +66,7 @@ db.json
 #webpack dev build
 assets
 webpack
+dist
 
 #cordova build
 platforms/

+ 46 - 0
server/.vscode/launch.json

@@ -0,0 +1,46 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Launch",
+            "type": "node",
+            "request": "launch",
+            "program": "${workspaceRoot}/src/server.js",
+            "stopOnEntry": false,
+            "args": [],
+            "cwd": "${workspaceRoot}",
+            "preLaunchTask": null,
+            "runtimeExecutable": null,
+            "runtimeArgs": [
+                "--nolazy",
+                "--require",
+                "@babel/register"
+            ],
+            "envFile": "${workspaceFolder}/../.env.development",
+            "console": "internalConsole",
+            "sourceMaps": true,
+            "outFiles": []
+        },
+        {
+            "name": "Attach",
+            "type": "node",
+            "request": "attach",
+            "port": 5858,
+            "address": "localhost",
+            "restart": false,
+            "sourceMaps": false,
+            "outFiles": [],
+            "localRoot": "${workspaceRoot}",
+            "remoteRoot": null
+        },
+        {
+            "name": "Attach to Process",
+            "type": "node",
+            "request": "attach",
+            "processId": "${command.PickProcess}",
+            "port": 5858,
+            "sourceMaps": false,
+            "outFiles": []
+        }
+    ]
+}

+ 0 - 97
server/game-server/online-duel-sync.js

@@ -1,97 +0,0 @@
-const fs = require('fs');
-
-export default function OnlineDuelSync(
-  ioServer,
-  mariadbConnector,
-  gameId,
-  firstPlayer
-) {
-  let io = ioServer;
-  let mdb = mariadbConnector;
-  let players = new Map();
-  players.set(firstPlayer.playerName, firstPlayer);
-
-  let loadGameData = function() {
-    let gameDetails = {};
-    try {
-      gameDetails = mdb.getGameById(gameId);
-    } catch (err) {
-      console.log('Error retreiving game : ', err.message);
-    }
-    return gameDetails;
-  };
-
-  let chat = function(name, message) {
-    io.to(gameId).emit('message', {
-      from: name,
-      text: message
-    });
-  };
-
-  firstPlayer
-    .getSocket()
-    .join(gameId, () => {
-      chat(
-        firstPlayer.playerName,
-        firstPlayer.playerName +
-          ' has entered the game, waiting for second player...'
-      );
-    })
-    .emit('update-game-data', loadGameData().game_data)
-    .on('chat', message => {
-      chat(firstPlayer.playerName, message);
-    });
-
-  let addPlayer = function(player) {
-    let res = true;
-    if (players.size === 1) {
-      if (players.has(player.playerName)) {
-        console.log(
-          'ERROR :  this player is already there ! ',
-          player.playerName
-        );
-        return false;
-      }
-      players.set(player.playerName, player);
-      player
-        .getSocket()
-        .join(gameId, () => {
-          chat(player.playerName, player.playerName + ' has entered the game');
-        })
-        .emit('update-game-data', loadGameData().game_data)
-        .on('chat', message => {
-          chat(player.playerName, message);
-        });
-    } else {
-      console.log(
-        'Already 2 players, weird :' +
-          player.playerName +
-          ' was trying to join : ',
-        players
-      );
-      res = false;
-    }
-    return res;
-  };
-
-  let playerLeft = function(player, disconnected) {
-    let playerLeaving = players.get(player.playerName);
-    if (playerLeaving) {
-      if (!disconnected) {
-        playerLeaving.getSocket().leave(gameId);
-      }
-
-      chat(player.playerName, player.playerName + ' has left the game');
-      players.delete(player.playerName);
-    }
-  };
-
-  let hasPlayers = function() {
-    return players.size > 0;
-  };
-  return {
-    addPlayer,
-    playerLeft,
-    hasPlayers
-  };
-}

File diff suppressed because it is too large
+ 868 - 17
server/package-lock.json


+ 6 - 2
server/package.json

@@ -7,8 +7,8 @@
   ],
   "scripts": {
     "start": "babel-node server.js",
-    "server-dev": "babel-node -r ../node_modules/dotenv/config server.js dotenv_config_path=../.env.development",
-    "tools": "node tools/server-tools.js"
+    "server-dev": "babel-node -r ../node_modules/dotenv/config src/server.js dotenv_config_path=../.env.development",
+    "tools": "node src/tools/server-tools.js"
   },
   "author": "JOJO",
   "license": "MIT",
@@ -17,9 +17,13 @@
     "socket.io": "^2.3.0"
   },
   "devDependencies": {
+    "@babel/cli": "^7.8.4",
+    "@babel/core": "^7.9.6",
     "@babel/node": "7.6.2",
     "@babel/plugin-proposal-object-rest-spread": "^7.9.0",
+    "@babel/plugin-transform-runtime": "^7.9.6",
     "@babel/preset-env": "7.6.2",
+    "@babel/register": "^7.9.0",
     "socket.io-client": "^2.3.0"
   }
 }

+ 1 - 1
src/server-shared/all-heroes.json → server/src/client-server-shared/all-heroes.json

@@ -1,5 +1,5 @@
 {
-    "version": "1.0.0",
+    "version": "2.0.0",
     "heroes": [
       {
         "name" : "Paysan",

+ 156 - 0
server/src/client-server-shared/duel-controller.js

@@ -0,0 +1,156 @@
+'use strict';
+
+/* This file will be used by server for online game, and by app for local game
+Therfore it should not have any dependency on any of those environments. 
+
+It is a state machine that follows the game steps
+
+It receives a dataStore to get and share data but doesn't care what is 
+behind (socket.io comm for online, direct call to vuex store for local)
+Is gives an interface in order to receive notifications
+
+Receiving interface : 
+  - dataStore : provides methods :
+    - .load()          : to get saved game data
+    - .save(data)      : to store game data and propagate (returns true if it succeeded, false otherwise)
+    - .getHeroesJson() : to get JSON file with all heroes cards described 
+   
+Provides interface :
+  - duelControllerProxy : provides methods :
+    - endTurn(data) : notifies the controller that players ended current turn with attached data */
+
+import { initHeroesFromJson } from './heroesHelper';
+import gameStates from './gameStates/gameStates';
+let DuelController = function(dataStore) {
+  let gameData = {
+    game: {},
+    bluePlayer: {},
+    redPlayer: {}
+  };
+  let allHeroes = null;
+
+  /** ***** STATE MACHINE internal methods ***** */
+
+  // Holds current running state
+  let currentGameState = null;
+
+  // Set new current State
+  let setCurrentState = stateName => {
+    currentGameState = states[stateName];
+    gameData.game.gameState = stateName;
+    console.log('Set new state : ', stateName);
+  };
+
+  // get new State
+  let getCurrentState = () => {
+    return currentGameState;
+  };
+
+  // Start the current state
+  let startCurrentState = (payload = null) => {
+    currentGameState.start(payload);
+  };
+
+  // Update the current state
+  let updateCurrentState = (payload = null) => {
+    currentGameState.update(payload);
+  };
+
+  // End the current state
+  let endCurrentState = (payload = null) => {
+    currentGameState.end(payload);
+  };
+
+  let getGameData = () => gameData;
+
+  // Interface to set data and store it
+  let storeData = (data = null) => {
+    if (data) {
+      setData(data);
+    }
+    return dataStore.save(gameData);
+  };
+
+  // Interface to set data only
+  let setData = data => {
+    gameData = data;
+  };
+
+  // Interface to load data
+  let loadData = () => {
+    gameData = dataStore.load();
+  };
+
+  // Interface to get set of all heroes
+  let getAllHeroes = () => {
+    if (!allHeroes) {
+      allHeroes = initHeroesFromJson(dataStore.getHeroesJson());
+    }
+    return allHeroes;
+  };
+
+  // Pass those state machine methods to each state
+  let stateCtrl = {
+    setCurrentState,
+    getCurrentState,
+    startCurrentState,
+    updateCurrentState,
+    endCurrentState,
+    storeData,
+    setData,
+    getGameData,
+    loadData,
+    getAllHeroes
+  };
+
+  // declare all states
+  // First state to be set after public APIs (start or resume game) are called
+  let states = {
+    '0_INIT': new gameStates.initState(stateCtrl),
+    '1_SELECT_FACTION': new gameStates.selectFactionState(stateCtrl),
+    '1_SELECT_DRAFT': new gameStates.selectDraftState(stateCtrl),
+    '1_SELECT_TOURNAMENT': new gameStates.selectTournamentState(stateCtrl),
+    '2_CHANGE_UP_TO_3_CARDS': new gameStates.changeUpTo3Cards(stateCtrl)
+  };
+
+  /** ***** Following are public methods ***** */
+
+  // Method to create and start a new game (both players are there)
+  let startNewGame = function(player1Name, player2Name, gameOptions) {
+    // 1st state is init
+    setCurrentState('0_INIT');
+    //Start it
+    startCurrentState({
+      player1Name,
+      player2Name,
+      gameOptions
+    });
+  };
+
+  // Method to resume existing game, gameData is passed already
+  let resumeGame = function(data) {
+    gameData = data;
+    // Just set state per data in DB
+    // This state will wait for update
+    setCurrentState(gameData.game.gameState);
+  };
+
+  // Provide an interface so that players can notify the DuelController
+  let DuelControllerProxy = () => {
+    return {
+      endTurn: data => {
+        console.log('data from player :>> ', data);
+        currentGameState.update(data);
+      }
+    };
+  };
+
+  // Return public methods
+  return {
+    startNewGame,
+    resumeGame,
+    duelControllerProxy: new DuelControllerProxy()
+  };
+};
+
+export default DuelController;

+ 55 - 0
server/src/client-server-shared/gameStates/0-initState.js

@@ -0,0 +1,55 @@
+'use strict';
+export default function(duelController) {
+  let duelCtrl = duelController;
+  let start = (payload = null) => {
+    if (payload) {
+      duelCtrl.setData({
+        bluePlayer: {
+          name: payload.player1Name,
+          color: 'blue'
+        },
+        redPlayer: {
+          name: payload.player2Name,
+          color: 'red'
+        },
+        game: {
+          allHeroes: duelCtrl.getAllHeroes(),
+          deckMode: payload.gameOptions.deck,
+          advRules: payload.gameOptions.advRules,
+          'waitingFor/blue': false,
+          'waitingFor/red': false,
+          currentPlayer: '',
+          'battleTiles/left': [{ id: 0, name: 'mine', redPoints: 3 }]
+        }
+      });
+      // Init state just initialize game data, end it
+      duelCtrl.endCurrentState();
+    }
+  };
+  let update = () => {};
+  let end = () => {
+    switch (duelCtrl.getGameData().game.deckMode) {
+      case 'faction':
+        duelCtrl.setCurrentState('1_SELECT_FACTION');
+        duelCtrl.startCurrentState();
+        break;
+      case 'tournament':
+        duelCtrl.setCurrentState('1_SELECT_TOURNAMENT');
+        duelCtrl.startCurrentState();
+        break;
+
+      case 'draft':
+        duelCtrl.setCurrentState('1_SELECT_DRAFT');
+        duelCtrl.startCurrentState();
+        break;
+
+      default:
+        break;
+    }
+  };
+  return {
+    start,
+    update,
+    end
+  };
+}

+ 89 - 0
server/src/client-server-shared/gameStates/1-selectDraftState.js

@@ -0,0 +1,89 @@
+'use strict';
+import { shuffleHeroes, getDraftSets } from '../heroesHelper';
+export default function selectDraftState(duelController) {
+  let duelCtrl = duelController;
+  let draftSets = [];
+  // For draft there are two turns, we are at 0
+  let draftTurn = 0;
+  //Blue will get first 6 cards, and red following 6
+  let blueDraftSetnb = 0;
+  let redDraftSetnb = 1;
+
+  let start = () => {
+    let gameData = duelCtrl.getGameData();
+    draftSets = getDraftSets(
+      gameData.game.allHeroes,
+      gameData.game.advRules.includes('popularity')
+    );
+    draftTurn = 1;
+
+    gameData.bluePlayer.draftHeroesIds =
+      draftSets[draftTurn - 1][blueDraftSetnb];
+    gameData.redPlayer.draftHeroesIds = draftSets[draftTurn - 1][redDraftSetnb];
+    gameData.game['waitingFor/blue'] = true;
+    gameData.game['waitingFor/red'] = true;
+    gameData.game.currentPlayer = 'both';
+    duelCtrl.storeData(gameData);
+  };
+
+  // Should receive in paylod : {color, twelveHeroes [{id, position,possibleActions}] of size 2 to 12, draftHeroesIds}
+  let update = (payload = null) => {
+    let gameData = duelCtrl.getGameData();
+    if (payload) {
+      let player = payload.color + 'player';
+      gameData[player].twelveHeroes = payload.twelveHeroes;
+      gameData[player].draftHeroesIds = payload.draftHeroesIds;
+      gameData.game['waitingFor/' + payload.color] = false;
+      duelCtrl.storeData(gameData);
+    }
+
+    // We finished waiting for both players to select 2 cards
+    if (
+      gameData.game['waitingFor/blue'] === false &&
+      gameData.game['waitingFor/red'] === false
+    ) {
+      // Check if players completed their deck
+      if (
+        gameData.bluePlayer.twelveHeroes.length == 12 &&
+        gameData.redPlayer.twelveHeroes.length == 12
+      ) {
+        shuffleHeroes(gameData.bluePlayer.twelveHeroes);
+        shuffleHeroes(gameData.redPlayer.twelveHeroes);
+        // End of state, decks are complete
+        duelCtrl.endCurrentState();
+      } else {
+        // Check if set of 6 cards is done
+        if (gameData.redPlayer.draftHeroesIds.length === 0) {
+          // Deal next 6 cards
+          draftTurn++;
+          gameData.bluePlayer.draftHeroesIds =
+            draftSets[draftTurn - 1][blueDraftSetnb];
+          gameData.redPlayer.draftHeroesIds =
+            draftSets[draftTurn - 1][redDraftSetnb];
+        } else {
+          // swap cards
+          let temp = gameData.bluePlayer.draftHeroesIds;
+          gameData.bluePlayer.draftHeroesIds =
+            gameData.redPlayer.draftHeroesIds;
+          gameData.redPlayer.draftHeroesIds = temp;
+        }
+        // Wait again for players to chose 2 more cards
+        gameData.game['waitingFor/blue'] = true;
+        gameData.game['waitingFor/red'] = true;
+        duelCtrl.storeData(gameData);
+      }
+    }
+  };
+
+  let end = () => {
+    console.log('Go to next game state : 2_CHANGE_UP_TO_3_CARDS');
+
+    duelCtrl.setCurrentState('2_CHANGE_UP_TO_3_CARDS');
+    duelCtrl.startCurrentState();
+  };
+  return {
+    start,
+    update,
+    end
+  };
+}

+ 56 - 0
server/src/client-server-shared/gameStates/1-selectFactionState.js

@@ -0,0 +1,56 @@
+'use strict';
+import { getHeroesIdsByFaction } from '../heroesHelper';
+export default function selectFactionState(duelController) {
+  let duelCtrl = duelController;
+  let start = () => {
+    let gameData = duelCtrl.getGameData();
+    gameData.game['waitingFor/blue'] = true;
+    gameData.game['waitingFor/red'] = true;
+    gameData.game.currentPlayer = 'both';
+    duelCtrl.storeData(gameData);
+  };
+
+  // Should receive in paylod : {color, faction}
+  let update = (payload = null) => {
+    let gameData = duelCtrl.getGameData();
+    if (payload) {
+      let player = payload.color + 'Player';
+      gameData[player].faction = payload.faction;
+      let randomHeroes = getHeroesIdsByFaction(
+        gameData.game.allHeroes,
+        payload.faction,
+        gameData.game.advRules.includes('popularity')
+      );
+      // Init player twelve heroes
+      gameData[player].twelveHeroes = [];
+      randomHeroes.forEach(id => {
+        gameData[player].twelveHeroes.push({
+          id,
+          position: 'pile',
+          possibleActions: []
+        });
+      });
+      gameData.game['waitingFor/' + payload.color] = false;
+      duelCtrl.storeData(gameData);
+    }
+
+    if (
+      gameData.game['waitingFor/blue'] === false &&
+      gameData.game['waitingFor/red'] === false
+    ) {
+      duelCtrl.endCurrentState();
+    }
+  };
+
+  let end = () => {
+    console.log('Go to next game state : 2_CHANGE_UP_TO_3_CARDS');
+
+    duelCtrl.setCurrentState('2_CHANGE_UP_TO_3_CARDS');
+    duelCtrl.startCurrentState();
+  };
+  return {
+    start,
+    update,
+    end
+  };
+}

+ 44 - 0
server/src/client-server-shared/gameStates/1-selectTournamentState.js

@@ -0,0 +1,44 @@
+'use strict';
+import { shuffleHeroes } from '../heroesHelper';
+export default function selectTournamentState(duelController) {
+  let duelCtrl = duelController;
+  let start = () => {
+    let gameData = duelCtrl.getGameData();
+    gameData.game['waitingFor/blue'] = true;
+    gameData.game['waitingFor/red'] = true;
+    gameData.game.currentPlayer = 'both';
+    duelCtrl.storeData(gameData);
+  };
+
+  // Should receive in paylod : {color, twelveHeroes [{id, position,possibleActions}] of size 12}
+  let update = (payload = null) => {
+    let gameData = duelCtrl.getGameData();
+    if (payload) {
+      let player = payload.color + 'player';
+      let randomHeroes = shuffleHeroes(payload.twelveHeroes);
+      // Init player twelve heroes
+      gameData[player].twelveHeroes = randomHeroes;
+      gameData.game['waitingFor/' + payload.color] = false;
+      duelCtrl.storeData(gameData);
+    }
+
+    if (
+      gameData.game['waitingFor/blue'] === false &&
+      gameData.game['waitingFor/red'] === false
+    ) {
+      duelCtrl.endCurrentState();
+    }
+  };
+
+  let end = () => {
+    console.log('Go to next game state : 2_CHANGE_UP_TO_3_CARDS');
+
+    duelCtrl.setCurrentState('2_CHANGE_UP_TO_3_CARDS');
+    duelCtrl.startCurrentState();
+  };
+  return {
+    start,
+    update,
+    end
+  };
+}

+ 41 - 0
server/src/client-server-shared/gameStates/2-changeUpTo3Cards.js

@@ -0,0 +1,41 @@
+'use strict';
+export default function changeUpTo3Cards(duelController) {
+  let duelCtrl = duelController;
+  let start = () => {
+    console.log('Start state 2');
+    let gameData = duelCtrl.getGameData();
+    console.log('gameData :>> ', gameData);
+    duelCtrl.storeData(gameData);
+  };
+
+  // Should receive in paylod : {color, faction}
+  let update = (payload = null) => {
+    let gameData = duelCtrl.getGameData();
+    if (payload) {
+      console.log('payload :>> ', payload);
+
+      /**  TODO : process player response */
+      gameData.game['waitingFor/' + payload.color] = false;
+      duelCtrl.storeData(gameData);
+    }
+
+    if (
+      gameData.game['waitingFor/blue'] === false &&
+      gameData.game['waitingFor/red'] === false
+    ) {
+      duelCtrl.endCurrentState();
+    }
+  };
+
+  let end = () => {
+    console.log('Go to next game state : 3...');
+
+    // duelCtrl.setCurrentState('3...');
+    // duelCtrl.startCurrentState();
+  };
+  return {
+    start,
+    update,
+    end
+  };
+}

+ 22 - 0
server/src/client-server-shared/gameStates/gameStates.js

@@ -0,0 +1,22 @@
+'use strict';
+// Following contains all states definitions
+// They all receive interface from duel controller as an argument (see stateCtrl in DuelController)
+// They all must implement :
+//  - start  : where it prepares the game data for this step and save it dor players
+//  - update : where it waits for players inputs, save them
+//  - end    : where it decides what should be next state
+// Those 3 methods might receive a payload but not necessarily
+// The state have also direct acces to game data
+
+import initState from './0-initState';
+import selectFactionState from './1-selectFactionState';
+import selectDraftState from './1-selectDraftState';
+import selectTournamentState from './1-selectTournamentState';
+import changeUpTo3Cards from './2-changeUpTo3Cards';
+export default {
+  initState,
+  selectFactionState,
+  selectDraftState,
+  selectTournamentState,
+  changeUpTo3Cards
+};

+ 85 - 0
server/src/client-server-shared/heroesHelper.js

@@ -0,0 +1,85 @@
+'use strict';
+function shuffle(a) {
+  for (let i = a.length - 1; i > 0; i--) {
+    const j = Math.floor(Math.random() * (i + 1));
+    [a[i], a[j]] = [a[j], a[i]];
+  }
+  return a;
+}
+
+export const initHeroesFromJson = function(allHeroesJson) {
+  let abilitiesMap = new Map();
+  allHeroesJson.abilities.forEach(ability => {
+    abilitiesMap.set(ability.abilityName, {
+      name: ability.abilityName,
+      hook: ability.abilityHook,
+      isOptionnal: ability.optionnal,
+      desc: ability['abilityDesc-FR']
+    });
+  });
+  let heroes = [];
+  let heroUniqueId = 0;
+  allHeroesJson.heroes.forEach(hero => {
+    let i = 0;
+    while (i < hero.nbInDeck) {
+      heroes.push({
+        id: heroUniqueId,
+        name: hero.name,
+        cost: hero.cost,
+        power: hero.power,
+        faction: hero.faction,
+        ability: abilitiesMap.get(hero.ability),
+        isDraftable: hero.draftMode,
+        popularity: hero.popularity
+      });
+      heroUniqueId++;
+      i++;
+    }
+  });
+  return heroes;
+};
+export const getHeroesIdsByFaction = function(
+  allHeroes,
+  faction,
+  popularityRule
+) {
+  let popularity = 'without';
+  if (popularityRule === true) {
+    popularity = 'with';
+  }
+  let heroIds = allHeroes
+    .filter(hero => {
+      return (
+        hero.faction === faction &&
+        (hero.popularity === popularity || hero.popularity === 'any')
+      );
+    })
+    .map(hero => hero.id);
+  return shuffle(heroIds);
+};
+export const getDraftSets = function(allHeroes, popularityRule) {
+  let popularity = 'without';
+  if (popularityRule === true) {
+    popularity = 'with';
+  }
+  let heroesDraftable = allHeroes
+    .filter(hero => {
+      return (
+        hero.isDraftable === true &&
+        (hero.popularity === popularity || hero.popularity === 'any')
+      );
+    })
+    .map(hero => hero.id);
+
+  let shuffledHeroes = shuffle(heroesDraftable);
+
+  // Return 4 sets of 6 heroes for draft mode
+  let draftSets = [
+    [shuffledHeroes.slice(0, 6), shuffledHeroes.slice(6, 12)],
+    [shuffledHeroes.slice(12, 18), shuffledHeroes.slice(18, 24)]
+  ];
+  return draftSets;
+};
+export const shuffleHeroes = function(heroes) {
+  return shuffle(heroes);
+};

+ 41 - 10
server/db/mariadb-connector.js → server/src/db/mariadb-connector.js

@@ -51,7 +51,6 @@ export default class MariadbConnector {
   }
 
   async addPlayer(username) {
-    console.log('mariadb add player : ', username);
     let conn;
     try {
       conn = await this.pool.getConnection();
@@ -59,7 +58,6 @@ export default class MariadbConnector {
         'INSERT INTO players value (?, NOW(),NULL)',
         [username]
       );
-      console.log('OK removing player : ', val);
 
       return val;
     } catch (err) {
@@ -72,14 +70,12 @@ export default class MariadbConnector {
     }
   }
   async removePlayer(username) {
-    console.log('mariadb remove player : ', username);
     let conn;
     try {
       conn = await this.pool.getConnection();
       const val = await conn.query('DELETE FROM players WHERE username=?', [
         username
       ]);
-      console.log('OK removing player : ', val);
       return val;
     } catch (err) {
       console.log('error removing player : ', err);
@@ -92,7 +88,6 @@ export default class MariadbConnector {
   }
 
   async addNewGame(game) {
-    console.log('mariadb add game : ', game);
     let conn;
     try {
       conn = await this.pool.getConnection();
@@ -108,7 +103,6 @@ export default class MariadbConnector {
         ]
       );
 
-      console.log('OK adding game : ', val);
       return val.insertId;
     } catch (err) {
       console.log('error adding game : ', err);
@@ -155,12 +149,10 @@ export default class MariadbConnector {
   }
 
   async removeGame(queryStr, queryArgs) {
-    console.log('mariadb remove game  : ', queryStr);
     let conn;
     try {
       conn = await this.pool.getConnection();
       const val = await conn.query(queryStr, queryArgs);
-      console.log('OK removing game : ', val);
       return val;
     } catch (err) {
       console.log('error removing game : ', err);
@@ -178,13 +170,12 @@ export default class MariadbConnector {
     try {
       conn = await this.pool.getConnection();
       const res = await conn.query(
-        "SELECT * FROM games WHERE (status = 'CREATED' OR status = 'PAUSED')  AND(player1 = ? OR player2 = '' OR player2 = ?)",
+        "SELECT * FROM games WHERE (status = 'CREATED' OR status = 'PAUSED' OR status = 'PLAYING')  AND(player1 = ? OR player2 = '' OR player2 = ?)",
         [username, username]
       );
       for (const { id, player1, player2, deck, adv_rules } of res) {
         games.push({ id, player1, player2, deck, adv_rules });
       }
-      console.log(`returning for ${username} : `, games);
     } finally {
       if (conn && conn !== null) {
         conn.end();
@@ -205,6 +196,46 @@ export default class MariadbConnector {
     }
     return game;
   }
+  async updateGameStatus(id, status, player2 = null) {
+    let conn;
+    let result = {};
+    try {
+      conn = await this.pool.getConnection();
+      if (player2) {
+        result = await conn.query(
+          'UPDATE games SET player2=?, status = ?, last_played = NOW() WHERE games.id =?',
+          [player2, status, id]
+        );
+      } else {
+        result = await conn.query(
+          'UPDATE games SET status = ?, last_played = NOW() WHERE games.id =?',
+          [status, id]
+        );
+      }
+    } finally {
+      if (conn && conn !== null) {
+        conn.end();
+      }
+    }
+    return result;
+  }
+  async updateGameData(id, data) {
+    let conn;
+    let result = {};
+    let dataJson = JSON.stringify(data);
+    try {
+      conn = await this.pool.getConnection();
+      result = await conn.query(
+        'UPDATE games SET game_data = ?, last_played = NOW() WHERE games.id =?',
+        [dataJson, id]
+      );
+    } finally {
+      if (conn && conn !== null) {
+        conn.end();
+      }
+    }
+    return result;
+  }
 }
 let convertAdvRulesToString = function(advRulesArray) {
   let advRulestStr = '';

+ 35 - 4
server/game-server/games-manager.js → server/src/game-server/games-manager.js

@@ -1,18 +1,45 @@
+'use strict';
 import OnlineDuelSync from './online-duel-sync';
-
+const fs = require('fs');
+const path = require('path');
 export default function GamesManager(ioServer, mariadbConn) {
   let currentGames = new Map();
 
+  // We create Heroes data from JSON file, it will be needed by the duelController
+  // DuelController has no dependency to browser env nor node env. So it cannot read the
+  // file itself (not same mechanism, in node we use fs readFileSync)
+  // Read once here for all the games to come
+  let allHeroesJson = JSON.parse(
+    fs.readFileSync(
+      path.resolve(__dirname, '../client-server-shared/all-heroes.json')
+    )
+  );
+
   // Method to add player into a game, create or add to game syncer
-  let addPlayerInGame = function(player, gameId, joinCreatedGame) {
+  let addPlayerInGame = function(
+    player,
+    gameId,
+    joinCreatedGame,
+    isNewGame = false,
+    gameOptions = {}
+  ) {
     if (currentGames.has(gameId)) {
       player.isPlayingGameId = gameId;
-      return currentGames.get(gameId).addPlayer(player);
+      let duelSync = currentGames.get(gameId);
+      return duelSync.addPlayer(player);
     } else if (joinCreatedGame === false) {
       player.isPlayingGameId = gameId;
       currentGames.set(
         gameId,
-        new OnlineDuelSync(ioServer, mariadbConn, gameId, player)
+        new OnlineDuelSync(
+          ioServer,
+          mariadbConn,
+          gameId,
+          player,
+          isNewGame,
+          gameOptions,
+          allHeroesJson
+        )
       );
       return true;
 
@@ -33,6 +60,10 @@ export default function GamesManager(ioServer, mariadbConn) {
     if (currentGames.has(id)) {
       currentGames.get(id).playerLeft(player, disconnected);
       if (!currentGames.get(id).hasPlayers()) {
+        // If it was new created game, don't set it to pause, game will be removed
+        if (!currentGames.get(id).isNewlyCreatedGame()) {
+          mariadbConn.updateGameStatus(id, 'PAUSED');
+        }
         currentGames.delete(id);
       }
     }

+ 191 - 0
server/src/game-server/online-duel-sync.js

@@ -0,0 +1,191 @@
+'use strict';
+import DuelController from '../client-server-shared/duel-controller';
+
+export default function OnlineDuelSync(
+  ioServer,
+  mariadbConnector,
+  gameId,
+  firstPlayer,
+  isNewGame,
+  gameOptions,
+  allHeroesJson
+) {
+  let io = ioServer;
+  let mdb = mariadbConnector;
+  let isNew = isNewGame;
+  let heroesJson = allHeroesJson;
+  let players = new Map();
+
+  // internal method to load game data from DB
+  let loadGameData = async function() {
+    let gameDetails = { game_data: {} };
+    try {
+      let game = await mdb.getGameById(gameId);
+      gameDetails = game[0];
+      try {
+        gameDetails.game_data = JSON.parse(game[0].game_data);
+      } catch (err) {
+        console.log('err converting JSON data :>> ', err);
+        gameDetails.game_data = {};
+      }
+    } catch (err) {
+      console.log('Error retreiving game : ', err.message);
+    }
+    return gameDetails;
+  };
+
+  // Internal method to send message to game room
+  let chat = function(name, message) {
+    io.to(gameId).emit('message', {
+      from: name,
+      text: message
+    });
+  };
+
+  //dataStore will be used by the duel controller to save data and notify
+  let dataStore = {
+    load() {},
+    // Save new game data after statemachine processed it
+    save(data) {
+      try {
+        mdb.updateGameData(gameId, data);
+        io.to(gameId).emit('update-game-data', data);
+        return true;
+      } catch (err) {
+        return false;
+      }
+    },
+    getHeroesJson() {
+      return heroesJson;
+    }
+  };
+
+  // Create Duel Controller (game state machine)
+  // Give it possibility to store data and notify players with dataStore
+  let duelController = new DuelController(dataStore);
+
+  // Is used to notify duel controller of a player action
+  // gives access to method : .endTurn(data)
+  let duelCtrlProxy = duelController.duelControllerProxy;
+
+  // Set array of players with first one that joined
+  players.set(firstPlayer.playerName, firstPlayer);
+
+  let game_data = {};
+  loadGameData().then(gameDetails => {
+    game_data = gameDetails.game_data;
+    // Join 1st player to game room and notify it
+    // Also send the game data (might be empty for new game but it is ok)
+    firstPlayer
+      .getSocket()
+      .join(gameId, () => {
+        chat(
+          firstPlayer.playerName,
+          firstPlayer.playerName +
+            ' has entered the game, waiting for second player...'
+        );
+      })
+      .emit('update-game-data', game_data)
+      // This allows the player to send message to the game room
+      .on('chat', message => {
+        chat(firstPlayer.playerName, message);
+      })
+      .on('end-turn', data => {
+        duelCtrlProxy.endTurn(data);
+      });
+
+    // If not new game, set status to PLAYING right now
+    // Otherwise we wait second player before doing so
+    if (!isNew) {
+      mdb.updateGameStatus(gameId, 'PLAYING');
+      // Resume game right away, we don't need second player yet as it might be 1st player turn
+      duelController.resumeGame(game_data);
+    }
+  });
+
+  // Public method to add a second player to the game
+  let addPlayer = function(player) {
+    let res = true;
+    // Doing some checks
+    if (players.size === 1) {
+      if (players.has(player.playerName)) {
+        console.log(
+          'ERROR :  this player is already there ! ',
+          player.playerName
+        );
+        return false;
+      }
+      players.set(player.playerName, player);
+      loadGameData().then(gameDetails => {
+        game_data = gameDetails.game_data;
+        // second player is in ! notify the game room and send latest game data (ok if empty)
+        player
+          .getSocket()
+          .join(gameId, () => {
+            chat(
+              player.playerName,
+              player.playerName + ' has entered the game'
+            );
+          })
+          .emit('update-game-data', game_data)
+          // Allow 2nd player to chat in the room
+          .on('chat', message => {
+            chat(player.playerName, message);
+          })
+          .on('end-turn', data => {
+            duelCtrlProxy.endTurn(data);
+          });
+
+        // Both players connected to a new game, set status to PLAYING
+        if (isNew) {
+          isNew = false;
+          mdb.updateGameStatus(gameId, 'PLAYING', player.playerName);
+
+          // Start the duel controller
+          duelController.startNewGame(
+            firstPlayer.playerName,
+            player.playerName,
+            gameOptions
+          );
+        }
+      });
+    } else {
+      console.log(
+        'Already 2 players, weird :' +
+          player.playerName +
+          ' was trying to join : ',
+        players
+      );
+      res = false;
+    }
+    return res;
+  };
+
+  // Handle player disconnection / reconnection without impacting other player
+  // If no player left, this object will be destroyed by games-manager
+  let playerLeft = function(player, disconnected) {
+    let playerLeaving = players.get(player.playerName);
+    if (playerLeaving) {
+      if (!disconnected) {
+        playerLeaving.getSocket().leave(gameId);
+      }
+
+      chat(player.playerName, player.playerName + ' has left the game');
+      players.delete(player.playerName);
+    }
+  };
+
+  // Public methods to know if there are currently any player tie to this game
+  let hasPlayers = function() {
+    return players.size > 0;
+  };
+  let isNewlyCreatedGame = function() {
+    return isNew;
+  };
+  return {
+    addPlayer,
+    playerLeft,
+    hasPlayers,
+    isNewlyCreatedGame
+  };
+}

+ 0 - 0
server/players/player-id.js → server/src/players/player-id.js


+ 23 - 26
server/server.js → server/src/server.js

@@ -18,8 +18,6 @@ function Server() {
   let authorizedPlayersNames = new Set();
 
   server.on('connection', function(socket) {
-    console.log('A player connected with id : ' + socket.id);
-
     socket.on('disconnect', reason => {
       console.log('A player disconnected, reason : ' + reason);
       if (
@@ -31,10 +29,6 @@ function Server() {
         if (username) {
           let player = authorizedPlayers.get(username);
 
-          // First, tell game manager a player left, is he was playing
-          if (player.isPlayingGameId >= 0) {
-            gamesManager.playerLeft(player, true);
-          }
           // If player had created a game, remove it
           removeAllGamesCreatedByPlayer(username)
             .then(res => {
@@ -44,6 +38,10 @@ function Server() {
               }
             })
             .catch(err => console.log('Error removing game :>> ', err.message));
+          // Tell game manager a player left, if he was playing
+          if (player.isPlayingGameId >= 0) {
+            gamesManager.playerLeft(player, true);
+          }
           connectedPlayers.delete(socket.id);
           player.setConnected(false);
           player.setSocket(null);
@@ -51,9 +49,7 @@ function Server() {
         }
       }
     });
-    socket.on('connect', () => {
-      console.log('Connected to server');
-    });
+
     socket.on('auth', async (playerName, callback) => {
       console.log(' Received auth message, player name : ' + playerName);
       await updatePlayersFromDb();
@@ -100,7 +96,7 @@ function Server() {
         authorizedPlayers.get(playerName).setSocket(socket);
         connectedPlayers.set(socket.id, playerName);
       }
-
+      console.log('auth called, result : ', response.message);
       callback(response);
       if (kickout === true) {
         setTimeout(() => {
@@ -110,7 +106,6 @@ function Server() {
     });
 
     socket.on('games-list', async (playerName, callback) => {
-      console.log(' Received games-list message, player name : ' + playerName);
       let response = {};
       try {
         let games = await getJoinableGames(playerName);
@@ -138,22 +133,24 @@ function Server() {
         gamesManager.addPlayerInGame(
           authorizedPlayers.get(connectedPlayers.get(socket.id)),
           id,
-          false
+          false,
+          true,
+          { deck: game.deck, advRules: game.advRules }
         );
-        console.log('Force all clients to reload their games');
         forceClientsReloadGames();
       } catch (error) {
-        console.log('error in create-game:>> ', error);
+        console.log('In create-game catch error :>> ', error);
         response = {
           res: 'ko',
           message: 'Error from server'
         };
       }
+      console.log('create-game called, result : ', response.message);
       callback(response);
     });
 
     socket.on('join-game', async (gameDetails, callback) => {
-      let result = gamesManager.addPlayerInGame(
+      let result = await gamesManager.addPlayerInGame(
         authorizedPlayers.get(connectedPlayers.get(socket.id)),
         gameDetails.id,
         gameDetails.joinCreatedGame
@@ -169,17 +166,19 @@ function Server() {
       callback(response);
     });
 
-    socket.on('leave-game', async (username, callback) => {
+    socket.on('leave-game', (username, callback) => {
       // Remove player from game he is playing
+      removeAllGamesCreatedByPlayer(username)
+        .then(res => {
+          if (res.affectedRows > 0) {
+            forceClientsReloadGames();
+          }
+        })
+        .catch(err => {
+          console.log('Error removing games : ' + err.message);
+        });
+
       gamesManager.playerLeft(authorizedPlayers.get(username), false);
-      try {
-        let res = removeAllGamesCreatedByPlayer(username);
-        if (res.affectedRows > 0) {
-          forceClientsReloadGames();
-        }
-      } catch (err) {
-        console.log('Error removing games : ' + err.message);
-      }
       callback(true);
     });
   });
@@ -188,10 +187,8 @@ function Server() {
     try {
       let usernames = await mariadbConn.getUsernames();
       authorizedPlayersNames = new Set(usernames);
-      console.log('authorizedPlayersNames update : ', authorizedPlayersNames);
       authorizedPlayersNames.forEach(name => {
         if (!authorizedPlayers.has(name)) {
-          console.log('New username authorized :', name);
           authorizedPlayers.set(name, new PlayerId(name));
         }
       });

+ 12 - 12
server/tools/server-tool-listener.js → server/src/tools/server-tool-listener.js

@@ -4,48 +4,48 @@ import io from 'socket.io';
 
 export default class ServerToolListner {
   constructor(server) {
-    this.server=server;
+    this.server = server;
   }
 
   listen(port) {
     let server = this.server;
     this.serverTool = io.listen(port);
-    this.serverTool.on('connection', function (socket) {
+    this.serverTool.on('connection', function(socket) {
       console.log('tool connected');
 
-      socket.on('players', (option,callback) =>  {
+      socket.on('players', (option, callback) => {
         callback(server.getCurrentListAuthorizedPlayers());
         socket.disconnect(false);
       });
-      socket.on('syncdb', async (option,callback) =>  {
+      socket.on('syncdb', async (option, callback) => {
         let response = await server.updatePlayersFromDb();
         callback(response);
         socket.disconnect(false);
       });
 
-      socket.on('add-player', async (username,callback) =>  {
+      socket.on('add-player', async (username, callback) => {
         let response = await server.addPlayerDb(...username);
         callback(response);
         socket.disconnect(false);
       });
-      socket.on('remove-player', async (username,callback) =>  {
+      socket.on('remove-player', async (username, callback) => {
         let response = await server.removePlayerDb(...username);
         callback(response);
         socket.disconnect(false);
       });
 
-      socket.on('add-game', async (game,callback) =>  {
-        let response='';
+      socket.on('add-game', async (game, callback) => {
+        let response = '';
         try {
           response = await server.addGameDb(...game);
-          response = "OK - inserted at id :" + response;
-        } catch(err) {
-          response= 'KO ' + err.message;
+          response = 'OK - inserted at id :' + response;
+        } catch (err) {
+          response = 'KO ' + err.message;
         }
         callback(JSON.stringify(response));
         socket.disconnect(false);
       });
-      socket.on('remove-game', async (options,callback) =>  {
+      socket.on('remove-game', async (options, callback) => {
         let response = await server.removeGameDb(...options);
         callback(response);
         socket.disconnect(false);

+ 15 - 16
server/tools/server-tools.js → server/src/tools/server-tools.js

@@ -1,7 +1,7 @@
 'use strict';
 
 // process.exit(1);
-var usage = function () {
+var usage = function() {
   var text = `
     USAGE : npm run tools <option>
       with <option> :
@@ -32,26 +32,25 @@ if (args.length === 0) {
   usage();
   process.exit(0);
 }
-var command = "";
-var option = "";
+var command = '';
+var option = '';
 switch (args[0]) {
-  case "players":
-  case "syncdb":
+  case 'players':
+  case 'syncdb':
     command = args[0];
     break;
 
-  case "add-player":
-  case "remove-player":
-  case "add-game":
-  case "remove-game":
-
-    if (args[0] === "remove-game" && args.length < 3) {
+  case 'add-player':
+  case 'remove-player':
+  case 'add-game':
+  case 'remove-game':
+    if (args[0] === 'remove-game' && args.length < 3) {
       usage();
       process.exit(0);
     }
-    if (args[1] && args[1] !== "") {
+    if (args[1] && args[1] !== '') {
       command = args[0];
-      if (args[0] === "add-game") {
+      if (args[0] === 'add-game') {
         try {
           option = [JSON.parse(args[1])];
         } catch (error) {
@@ -73,10 +72,10 @@ switch (args[0]) {
     break;
 }
 
-if (command !== "") {
+if (command !== '') {
   console.log('Connecting to server...');
-  var socket = require('socket.io-client')("http://localhost:1664");
-  socket.emit(command, option,function (response) {
+  var socket = require('socket.io-client')('http://localhost:1664');
+  socket.emit(command, option, function(response) {
     console.log('Done, response from server : ', response);
   });
 }

+ 75 - 24
src/App.vue

@@ -1,49 +1,100 @@
 <template>
   <div id="app" class="appBackground">
-    <keep-alive>
-      <component
-        :is="display"
-        @join-game-id="launchOnlineGame($event)"
-        :username="username"
-        :game-id="onlineGameId"
-      ></component>
-    </keep-alive>
+    <div class="container-fluid">
+      <div class="row">
+        <div
+          class="col-2"
+          v-if="!hideTest"
+          style="background-color:lightgreen;"
+        >
+          <span class="title-medieval">For tests</span>
+          <br />
+          <button
+            class="btn btn-primary custom-button"
+            @click="test = 'app-menu'"
+          >
+            Enter Menu
+          </button>
+          <br />
+          <br />
+          <button
+            class="btn btn-primary custom-button"
+            @click="test = 'app-game'"
+          >
+            Enter Game
+          </button>
+        </div>
+        <div class="col">
+          <button
+            style="font-size:8px"
+            @click="
+              hideTest = !hideTest;
+              if (hideTest) test = '';
+            "
+          >
+            Test
+          </button>
+          <keep-alive>
+            <component :is="choseDisplay"></component>
+          </keep-alive>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <script>
 import AppMenu from './menu/AppMenu';
 import AppGame from './game/AppGame';
-import { globalEventsBus } from './main';
+import { mapGetters } from 'vuex';
 
 export default {
   name: 'App',
   data() {
     return {
-      display: 'app-menu',
-      username: '',
-      onlineGameId: -1,
-      hideTest: true
+      hideTest: true,
+      test: ''
     };
   },
   components: {
     AppMenu,
     AppGame
   },
-  methods: {
-    launchOnlineGame(game) {
-      console.log('received game to launch : ', game);
-      this.username = game.username;
-      this.onlineGameId = game.id;
-      this.display = 'app-game';
+  computed: {
+    ...mapGetters({
+      isGameRunning: 'isGameRunning'
+    }),
+    choseDisplay() {
+      if (this.test !== '') {
+        return this.test;
+      }
+      if (!this.isGameRunning) {
+        return 'app-menu';
+      } else {
+        return 'app-game';
+      }
+    }
+  },
+  beforeMount() {
+    let user = localStorage.getItem('username');
+    if (user) {
+      console.log('user in storage, try to connect ! : ', user);
+      this.$store
+        .dispatch('menu/connect', user)
+        .then(() => {
+          let gameId = localStorage.getItem('gameId');
+          if (gameId && gameId > -1) {
+            console.log('gameId in storage, try to join ! : ', gameId);
+            this.$store.dispatch('menu/joinOnlineGame', {
+              id: gameId,
+              isNew: false
+            });
+          }
+        })
+        .catch(() => {});
     }
   },
   created() {
-    globalEventsBus.$on('game-stopped', () => {
-      this.username = '';
-      this.onlineGameId = -1;
-      this.display = 'app-menu';
-    });
     this.$store.watch(
       () => this.$store.getters['messages'],
       messages => {

+ 97 - 11
src/common/heroes-display/HeroesDisplay.vue

@@ -1,9 +1,17 @@
 <template>
   <div>
-    <button class="btn btn-primary" @click="showFilter = !showFilter">
+    <button
+      class="btn btn-primary"
+      @click="showFilter = !showFilter"
+      v-if="display12heroesOnly === false"
+    >
       Show/Hide filter
     </button>
-    <button class="btn btn-primary" @click="resetFilter">
+    <button
+      class="btn btn-primary"
+      @click="resetFilter"
+      v-if="display12heroesOnly === false"
+    >
       Reset filter
     </button>
     <heroes-filter
@@ -11,16 +19,21 @@
       :filter-options="filterOptions"
       v-model="filter"
     ></heroes-filter>
+    <heroes-selector
+      v-if="selectHeroes"
+      :factionSelected="factionSelected"
+    ></heroes-selector>
     <hr />
-    <heroes-selector v-if="selectHeroes.enable"></heroes-selector>
     <div
       class="row row-cols-2 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 mt-3"
     >
       <hero
-        v-for="(hero, index) in heroesBy(filter)"
+        v-for="(hero, index) in heroesToDisplay"
         :key="index"
         :hero="hero"
-        :selectable="['draft', 'tournament'].includes(selectHeroes.mode)"
+        :selectable="
+          selectHeroes === true && ['draft', 'tournament'].includes(deckMode)
+        "
       ></hero>
       <!-- v-model="selected" -->
     </div>
@@ -48,11 +61,33 @@ let initialFilter = function(filterOptions) {
 };
 
 export default {
-  props: ['filterOptions', 'selectHeroes'],
+  // selectHeroes : if true it means we are not just displaying the cards
+  // we need to offer a way to user to select heroes based on current game mode
+  props: {
+    selectHeroes: {
+      type: Boolean,
+      default: false
+    },
+    display12heroesOnly: {
+      type: Boolean,
+      default: false
+    }
+  },
   data() {
     return {
-      showFilter: false,
-      filter: initialFilter(this.filterOptions)
+      showFilter: this.selectHeroes,
+      filter: {
+        minPower: 0,
+        maxPower: 6,
+        minCost: 0,
+        maxCost: 7,
+        faction: '',
+        popularity: '',
+        draft: '',
+        name: '',
+        byName: false,
+        byId: []
+      }
     };
   },
   components: {
@@ -62,15 +97,66 @@ export default {
   },
   computed: {
     ...mapGetters({
-      heroesBy: 'heroes/by',
-      allHeroes: 'heroes/all'
-    })
+      heroesBy: 'game/filterHeroes',
+      allHeroes: 'game/allHeroes',
+      deckMode: 'game/deckMode',
+      isPopularityRule: 'game/isPopularityRule',
+      myColor: 'game/myColor',
+      getSelectableDraftIds: 'game/myDraftIds',
+      getTwelveHeroes: 'game/myTwelveHeroes'
+    }),
+    filterOptions() {
+      // init filter options will all
+      let factionFilter = ['all', 'orcs', 'humans', 'elves', 'meca', 'none'];
+      let popularityFilter = ['without', 'with'];
+      let draftFilter = ['all', 'yes', 'no'];
+      if (this.selectHeroes === true) {
+        draftFilter = ['all'];
+        // if tournament and pop rule ON, let's authorize to select between all golems
+        if (this.isPopularityRule === true && this.deckMode !== 'tournament') {
+          popularityFilter = ['with'];
+        } else if (this.isPopularityRule === false) {
+          popularityFilter = ['without'];
+        }
+        if (this.deckMode === 'faction') {
+          factionFilter = ['orcs', 'humans', 'elves', 'meca'];
+        }
+      }
+      if (this.display12heroesOnly === true) {
+        popularityFilter = ['none'];
+      }
+      return {
+        faction: factionFilter,
+        popularity: popularityFilter,
+        draft: draftFilter,
+        power: true,
+        cost: true,
+        byName: true
+      };
+    },
+    heroesToDisplay() {
+      if (this.display12heroesOnly === true) {
+        return this.getTwelveHeroes;
+      }
+      if (this.selectHeroes === true) {
+        return this.heroesBy(this.filter);
+      } else {
+        return this.allHeroes;
+      }
+    },
+    factionSelected() {
+      return this.filter.faction;
+    }
   },
   methods: {
     resetFilter() {
       Object.assign(this.filter, initialFilter(this.filterOptions));
     }
   },
+  created() {
+    Object.assign(this.filter, initialFilter(this.filterOptions));
+    this.filter.byId = this.getSelectableDraftIds;
+  },
   watch: {
     'filter.byName'(newValue) {
       if (newValue === false) {

+ 1 - 0
src/common/heroes-display/components/HeroesFilter.vue

@@ -98,6 +98,7 @@
         :disabled="!value.byName"
       />
     </div>
+    <hr />
   </div>
 </template>
 

+ 42 - 2
src/common/heroes-display/components/HeroesSelector.vue

@@ -1,11 +1,51 @@
 <template>
   <div>
-    <hr />
+    <button
+      class="btn btn-primary"
+      v-if="deckMode === 'faction'"
+      :disabled="!isItMyTurn"
+      @click="submitFaction(factionSelected)"
+    >
+      {{ factionButtonText }}
+    </button>
+    <div v-if="deckMode === 'tournament'">
+      Here logic to select tournament deck
+    </div>
+    <div v-if="deckMode === 'draft'">
+      Here logic to select draft deck
+    </div>
   </div>
 </template>
 
 <script>
-export default {};
+import { mapGetters, mapActions } from 'vuex';
+export default {
+  props: ['factionSelected'],
+  computed: {
+    ...mapGetters({
+      deckMode: 'game/deckMode',
+      myColor: 'game/myColor',
+      isItMyTurn: 'game/isItMyTurn'
+    }),
+    factionButtonText() {
+      if (this.isItMyTurn === true) {
+        return 'Select Faction ' + this.factionSelected;
+      } else {
+        return 'Wait other player...';
+      }
+    }
+  },
+  methods: {
+    ...mapActions({
+      submitFaction(dispatch, faction) {
+        return dispatch(
+          'game/' + this.myColor + 'Player/submitFaction',
+          faction
+        );
+      }
+    })
+  }
+};
 </script>
 
 <style></style>

+ 19 - 3
src/common/socket-service.js

@@ -5,7 +5,6 @@ const SERVER_URL = 'http://' + process.env.VUE_APP_SERVER_HOST;
 const SERVER_PORT = process.env.VUE_APP_SERVER_PORT;
 
 export default function SocketService(socketEventBus, vuexStore) {
-  let eventBus = socketEventBus;
   let store = vuexStore;
   let ioClient = null;
   let connect = function(name) {
@@ -46,11 +45,16 @@ export default function SocketService(socketEventBus, vuexStore) {
 
       ioClient.on('reload-games-list', () => {
         console.log('force reload games list !');
-        eventBus.$emit('reload-games');
+        store.dispatch('menu/fetchJoinableGames');
       });
       ioClient.on('message', message => {
         store.dispatch('addMessageToQueue', message);
       });
+      ioClient.on('update-game-data', data => {
+        if (data && data.game) {
+          store.dispatch('game/update', data);
+        }
+      });
     });
     return promise;
   };
@@ -101,7 +105,6 @@ export default function SocketService(socketEventBus, vuexStore) {
     );
   };
   let joinGame = function(username, gameId, joinCreatedGame) {
-    console.log('gameId :>> ', gameId);
     return checkConnection(username).then(
       () => {
         return new Promise((resolve, reject) => {
@@ -131,6 +134,18 @@ export default function SocketService(socketEventBus, vuexStore) {
       error => Promise.reject(error)
     );
   };
+  let endTurn = function(payload) {
+    return checkConnection(payload.username).then(
+      () => {
+        return new Promise(resolve => {
+          ioClient.emit('end-turn', payload, function(response) {
+            resolve(response);
+          });
+        });
+      },
+      error => Promise.reject(error)
+    );
+  };
 
   let chat = function(message) {
     ioClient.emit('chat', message);
@@ -142,6 +157,7 @@ export default function SocketService(socketEventBus, vuexStore) {
     createGame,
     leaveGame,
     joinGame,
+    endTurn,
     chat
   };
 }

+ 38 - 64
src/game/AppGame.vue

@@ -2,22 +2,10 @@
   <div>
     <div class="row">
       <div class="col-12 col-md-8 offset-sm-2 col-lg-6 offset-md-3">
-        <h2>Launch game id {{ gameId }} for {{ username }}</h2>
-        <button
-          class="btn btn-danger"
-          v-if="isGameRunning"
-          @click="isGameRunning = !isGameRunning"
-        >
+        <button class="btn btn-danger" @click="stopGame">
           Stop game
         </button>
-        <button
-          class="btn btn-success"
-          v-else
-          @click="isGameRunning = !isGameRunning"
-        >
-          Start game
-        </button>
-        <input type="text" v-model="chatMessage" />
+        <input type="text" v-model="chatMessage" @keydown.enter="sendChat" />
         <button
           class="btn btn-primary"
           @click="sendChat"
@@ -28,79 +16,65 @@
       </div>
     </div>
     <hr />
-    <label>Deck mode to select heroes :</label>
-    <select
-      class="form-control pull-left"
-      v-model="deckSelectionMode"
-      style="width:180px"
-    >
-      <option selected>faction</option>
-      <option>draft</option>
-      <option>tournament</option>
-      <option value="display">just display</option>
-    </select>
-    <hr />
-    <heroes-display
-      :filter-options="filterOptions"
-      :select-heroes="{ enable: true, mode: deckSelectionMode }"
-    ></heroes-display>
+    <component :is="gameStepDisplay" :select-heroes="true"></component>
   </div>
 </template>
 
 <script>
-import { globalEventsBus } from '../main';
+import { mapGetters } from 'vuex';
 import { socketService } from '../main';
 import HeroesDisplay from '../common/heroes-display/HeroesDisplay';
-import { mapActions } from 'vuex';
+import OnlineWait from './components/OnlineWait';
+import GameBoard from './components/GameBoard';
 export default {
   components: {
-    HeroesDisplay
+    HeroesDisplay,
+    OnlineWait,
+    GameBoard
   },
-  props: ['game-id', 'username'],
   data() {
     return {
-      isGameRunning: false,
-      filterOptions: {
-        //  - faction (orcs humans elves meca none all)
-        //  - popularity (with without)
-        //  - draftMode (yes no all)
-        faction: ['all', 'orcs', 'humans', 'elves', 'meca', 'none'],
-        popularity: ['without', 'with'],
-        draft: ['all', 'yes', 'no'],
-        power: true,
-        cost: true,
-        byName: true
-      },
-      deckSelectionMode: 'faction',
       chatMessage: ''
     };
   },
-  watch: {
-    isGameRunning() {
-      if (this.isGameRunning) {
-        globalEventsBus.$emit('game-running');
-      } else {
-        socketService.leaveGame(this.username).catch(err => {
-          console.log('Error communicating to server to remove games : ' + err);
-        });
-        globalEventsBus.$emit('game-stopped');
+  computed: {
+    ...mapGetters({
+      gameState: 'game/state'
+    }),
+    gameStepDisplay() {
+      let componentToDisplay = '';
+      switch (this.gameState) {
+        case null:
+        case '':
+        case '0_INIT':
+          componentToDisplay = 'online-wait';
+          break;
+        case '1_SELECT_FACTION':
+        case '1_SELECT_DRAFT':
+        case '1_SELECT_TOURNAMENT':
+          componentToDisplay = 'heroes-display';
+          break;
+        case '2_CHANGE_UP_TO_3_CARDS':
+          componentToDisplay = 'game-board';
+          break;
+        default:
+          componentToDisplay = 'online-wait';
+          break;
       }
+      return componentToDisplay;
     }
   },
-  computed: {
-    ...mapActions({
-      setHeroesFromJson: 'heroes/setFromLocalJson'
-    })
-  },
   methods: {
+    stopGame() {
+      this.$store.dispatch('stopGame');
+      this.$store.dispatch('game/resetGameState');
+    },
     sendChat() {
       socketService.chat(this.chatMessage);
       this.chatMessage = '';
     }
   },
-  created() {
-    this.setHeroesFromJson.then();
-  }
+  created() {}
 };
 </script>
 

+ 20 - 0
src/game/components/GameBoard.vue

@@ -0,0 +1,20 @@
+<template>
+  <div>
+    <h1>Game board will be here</h1>
+    <h3>Next to come : replace up to 3 cards</h3>
+    <hr />
+    <h3>The 12 heroes in my deck :</h3>
+    <heroes-display :display12heroesOnly="true"></heroes-display>
+  </div>
+</template>
+
+<script>
+import HeroesDisplay from '../../common/heroes-display/HeroesDisplay';
+export default {
+  components: {
+    HeroesDisplay
+  }
+};
+</script>
+
+<style></style>

+ 13 - 0
src/game/components/OnlineWait.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    <h1>Wait...</h1>
+  </div>
+</template>
+
+<script>
+export default {
+  computed: {}
+};
+</script>
+
+<style></style>

+ 3 - 0
src/main.js

@@ -12,6 +12,9 @@ import SocketService from './common/socket-service';
 
 import { store } from './store/store';
 
+import Constants from './store/types';
+Vue.use(Constants);
+
 export const socketEventBus = new Vue({});
 export const socketService = new SocketService(socketEventBus, store);
 

+ 12 - 1
src/menu/AppMenu.vue

@@ -35,6 +35,7 @@
 import MenuLogin from './login/MenuLogin';
 import MenuOnlineRoom from './online-room/MenuOnlineRoom';
 import MenuGameCreation from './game-creation/MenuGameCreation';
+import { mapGetters } from 'vuex';
 export default {
   data() {
     return {
@@ -54,7 +55,6 @@ export default {
     },
     enterOnline(username) {
       this.previousPage.push(this.menuPage);
-      console.log('enter online with username : ' + username);
       this.username = username;
       this.menuPage = 'menu-online-room';
     },
@@ -62,6 +62,17 @@ export default {
       this.previousPage.push(this.menuPage);
       this.menuPage = 'menu-game-creation';
     }
+  },
+  computed: {
+    ...mapGetters({ isGameRunning: 'isGameRunning' })
+  },
+  watch: {
+    isGameRunning(value) {
+      if (value === true) {
+        this.menuPage = 'menu-login';
+        this.previousPage = [];
+      }
+    }
   }
 };
 </script>

+ 19 - 34
src/menu/game-creation/MenuGameCreation.vue

@@ -39,15 +39,15 @@
       <hr />
       <button
         class="btn btn-primary"
-        @click.prevent="createGame"
-        :disabled="creatingGame"
+        @click.prevent="createGame({ deckMode: selectedDeck, advRules })"
+        :disabled="createGameStatus.status === $types('request').REQUESTED"
       >
-        {{ createButtonText }}
+        {{ createGameStatus.status | getButtonText }}
       </button>
       <button
         class="btn btn-primary"
         @click.prevent="$emit('back')"
-        :disabled="creatingGame"
+        :disabled="createGameStatus.status === $types('request').REQUESTED"
       >
         Back
       </button>
@@ -56,7 +56,8 @@
 </template>
 
 <script>
-import { socketService } from '../../main';
+import { mapActions, mapGetters } from 'vuex';
+import types from '../../store/types';
 
 const decks = ['Faction', 'Draft', 'Tournament'];
 export default {
@@ -65,38 +66,22 @@ export default {
     return {
       deckChoices: decks,
       selectedDeck: decks[0].toLowerCase(),
-      advRules: [],
-      creatingGame: false,
-      createButtonText: 'Create'
+      advRules: []
     };
   },
+  computed: {
+    ...mapGetters({
+      createGameStatus: 'menu/createGameStatus'
+    })
+  },
   methods: {
-    createGame() {
-      this.creatingGame = true;
-      this.createButtonText = 'Creating';
-      console.log('create game, advrules : ', this.advRules);
-      let game = {
-        player1: this.username,
-        player2: '',
-        deck: this.selectedDeck,
-        advRules: this.advRules,
-        status: 'CREATED',
-        data: ''
-      };
-      socketService
-        .createGame(game)
-        .then(id => {
-          this.$emit('home');
-          this.$emit('join-game-id', {
-            id,
-            username: this.username
-          });
-        })
-        .catch(err => {
-          this.creatingGame = false;
-          this.createButtonText = 'Create';
-          alert('Could not create game (' + err + ')');
-        });
+    ...mapActions({
+      createGame: 'menu/createOnlineGame'
+    })
+  },
+  filters: {
+    getButtonText(status) {
+      return status === types.request.REQUESTED ? 'Creating' : 'Create';
     }
   }
 };

+ 52 - 66
src/menu/login/MenuLogin.vue

@@ -7,9 +7,13 @@
           placeholder="Username"
           autocomplete="off"
           class="inputUsername"
-          v-model="username"
-          :disabled="lockUsername"
-          @keydown.enter="connect()"
+          :value="username"
+          @input="usernameInput = $event.target.value"
+          :disabled="
+            connectionStatus.status === $types('request').REQUESTED ||
+              isConnected
+          "
+          @keydown.enter="connect(usernameInput)"
         />
       </b-col>
     </b-row>
@@ -17,10 +21,10 @@
       <b-col cols="auto">
         <button
           class="loginButtons hvr-pulse"
-          @click.prevent="connect"
-          :disabled="connectButton.disabled"
+          @click.prevent="connectButtonAction"
+          :disabled="connectionStatus.status === $types('request').REQUESTED"
         >
-          {{ connectButton.text }}
+          {{ connectButtonText }}
         </button>
       </b-col>
     </b-row>
@@ -34,7 +38,7 @@
       <b-col cols="auto">
         <button
           class="loginButtons onlineButton hvr-pulse"
-          :disabled="onlineButtonDisable || isOneGameRunning"
+          :disabled="!isConnected || isGameRunning"
           @click.prevent="$emit('enter-online', username)"
         >
           Online Game
@@ -45,8 +49,8 @@
       <b-col cols="auto">
         <button
           class="loginButtons hvr-pulse"
-          @click.prevent="test"
-          :disabled="isOneGameRunning"
+          @click.prevent="$emit('enter-local')"
+          :disabled="isGameRunning"
         >
           Local Game
         </button>
@@ -56,72 +60,54 @@
 </template>
 
 <script>
-import { socketService } from '../../main';
-import { globalEventsBus } from '../../main';
-function initialState() {
-  return {
-    connectButton: {
-      text: 'Connect',
-      disabled: false
-    },
-    connectionStatus: 'Not Connected',
-    username: '',
-    isConnected: false,
-    lockUsername: false,
-    onlineButtonDisable: true,
-    isOneGameRunning: false
-  };
-}
+import { mapActions, mapGetters } from 'vuex';
 
 export default {
   data() {
-    return initialState();
+    return {
+      usernameInput: this.username
+    };
   },
   methods: {
-    connect() {
-      if (!this.isConnected) {
-        if (this.username === '') {
-          this.connectionStatus = 'Enter a username';
-        } else {
-          this.connectButton.disabled = true;
-          this.connectButton.text = 'Connecting...';
-          this.lockUsername = true;
-          socketService
-            .connect(this.username)
-            .then(() => {
-              this.isConnected = true;
-              this.connectButton.text = 'Disconnect';
-              this.connectButton.disabled = false;
-              this.onlineButtonDisable = false;
-              this.connectionStatus = 'Connected as ' + this.username;
-            })
-            .catch(err => {
-              let user = this.username;
-              Object.assign(this.$data, initialState());
-              this.connectionStatus =
-                'Could not connect as ' + user + ' (' + err + ')';
-            });
-        }
+    ...mapActions({
+      connect: 'menu/connect',
+      disconnect: 'disconnect'
+    }),
+    connectButtonAction() {
+      if (this.isConnected) {
+        this.disconnect();
+      } else {
+        this.connect(this.usernameInput);
+      }
+    }
+  },
+  computed: {
+    ...mapGetters({
+      isGameRunning: 'isGameRunning',
+      username: 'username',
+      isConnected: 'isConnected',
+      connectionStatus: 'menu/connectionStatus'
+    }),
+    connectButtonText() {
+      if (this.isConnected === true) {
+        return 'Disconnect';
+      } else if (
+        this.connectionStatus.status === this.$types('request').REQUESTED
+      ) {
+        return 'Connecting';
       } else {
-        socketService.disconnect();
-        Object.assign(this.$data, initialState());
+        return 'Connect';
       }
-    },
-    test() {
-      socketService.test(this.username);
     }
   },
-  computed: {},
-  created() {
-    globalEventsBus.$on('game-running', () => {
-      this.connectionStatus =
-        'Connected as ' + this.username + ' (Game running)';
-      this.isOneGameRunning = true;
-    });
-    globalEventsBus.$on('game-stopped', () => {
-      this.connectionStatus = 'Connected as ' + this.username;
-      this.isOneGameRunning = false;
-    });
+  filters: {
+    getRunningText(isRunning) {
+      if (isRunning === true) {
+        return '(Game running)';
+      } else {
+        return '';
+      }
+    }
   }
 };
 </script>

+ 33 - 45
src/menu/online-room/MenuOnlineRoom.vue

@@ -6,12 +6,18 @@
           Existing Games
         </h1>
         <div id="online-games">
-          <div v-if="loading">
-            <h3>{{ loadingMessage }}</h3>
+          <div
+            v-if="
+              loadGamesStatus.status === $types('request').IDLE ||
+                loadGamesStatus.status === $types('request').REQUESTED ||
+                joinableGames.length == 0
+            "
+          >
+            <h3>{{ loadGamesStatus.text }}</h3>
           </div>
           <div v-on-clickaway="unselect" v-else>
             <game-item
-              v-for="(game, index) in gamesList"
+              v-for="(game, index) in joinableGames"
               :key="game.id"
               :game-element="game"
               @click.native="selectedIndex = index"
@@ -26,7 +32,16 @@
               <br />
               <b-icon icon="plus-circle"></b-icon>
             </button>
-            <button class="btn" :disabled="resumeDisabled" @click="joinGame()">
+            <button
+              class="btn"
+              :disabled="resumeDisabled"
+              @click="
+                joinGame({
+                  id: joinableGames[selectedIndex].id,
+                  isNew: joinableGames[selectedIndex].player2 === ''
+                })
+              "
+            >
               Join
               <br />
               <b-icon icon="controller"></b-icon>
@@ -46,16 +61,13 @@
 <script>
 import GameItem from './components/GameItem';
 import { mixin as clickaway } from 'vue-clickaway';
-import { socketService, socketEventBus } from '../../main';
+import { mapActions, mapGetters } from 'vuex';
 export default {
   props: ['username'],
   data() {
     return {
       selectedIndex: -1,
-      resumeDisabled: true,
-      loading: true,
-      loadingMessage: 'Loading...',
-      gamesList: []
+      resumeDisabled: true
     };
   },
   components: {
@@ -63,7 +75,6 @@ export default {
   },
   watch: {
     selectedIndex() {
-      console.log('index selected: ' + this.selectedIndex);
       if (this.selectedIndex >= 0) {
         this.resumeDisabled = false;
       } else {
@@ -72,47 +83,24 @@ export default {
     }
   },
   methods: {
+    ...mapActions({
+      joinGame: 'menu/joinOnlineGame',
+      fetchJoinableGames: 'menu/fetchJoinableGames'
+    }),
     unselect() {
       this.selectedIndex = -1;
-    },
-    joinGame() {
-      let index = this.selectedIndex;
-      let id = this.gamesList[index].id;
-      socketService
-        .joinGame(this.username, id, this.gamesList[index].player2 === '')
-        .then(() => {
-          this.$emit('back');
-          this.$emit('join-game-id', {
-            id,
-            username: this.username
-          });
-        })
-        .catch(err => {
-          console.log('error joining : ', err);
-        });
-    },
-    updateGamesList() {
-      socketService
-        .getGamesList(this.username)
-        .then(games => {
-          this.gamesList = games;
-          if (this.gamesList.length === 0) {
-            this.loadingMessage = 'No games found, wait a new or create one !';
-          } else {
-            this.loading = false;
-          }
-        })
-        .catch(() => {
-          this.loadingMessage = 'Error reaching server';
-        });
     }
   },
+  computed: {
+    ...mapGetters({
+      joinableGames: 'menu/joinableGames',
+      loadGamesStatus: 'menu/loadGamesStatus'
+    })
+  },
   mixins: [clickaway],
   created() {
-    this.updateGamesList();
-    socketEventBus.$on('reload-games', () => {
-      this.updateGamesList();
-    });
+    // When entering this component, fetch joignable games from server
+    this.fetchJoinableGames();
   }
 };
 </script>

+ 0 - 28
src/server-shared/heroesHelper.js

@@ -1,28 +0,0 @@
-export const initHeroesFromJson = function(allHeroesJson) {
-  let abilitiesMap = new Map();
-  allHeroesJson.abilities.forEach(ability => {
-    abilitiesMap.set(ability.abilityName, {
-      name: ability.abilityName,
-      hook: ability.abilityHook,
-      isOptionnal: ability.optionnal,
-      desc: ability['abilityDesc-FR']
-    });
-  });
-  let heroesSet = new Set();
-  allHeroesJson.heroes.forEach(hero => {
-    let i = 0;
-    while (i < hero.nbInDeck) {
-      heroesSet.add({
-        name: hero.name,
-        cost: hero.cost,
-        power: hero.power,
-        faction: hero.faction,
-        ability: abilitiesMap.get(hero.ability),
-        isDraftable: hero.draftMode,
-        popularity: hero.popularity
-      });
-      i++;
-    }
-  });
-  return heroesSet;
-};

+ 182 - 0
src/store/game/game.js

@@ -0,0 +1,182 @@
+import AnyPlayer from './player/any-player';
+import allHeroesJson from '../../../server/src/client-server-shared/all-heroes.json';
+
+const initGameState = {
+  gameState: '',
+  allHeroes: [],
+  deckMode: '',
+  advRules: [],
+  'waitingFor/blue': false,
+  'waitingFor/red': false,
+  currentPlayer: '',
+  'battleTiles/left': [],
+  'battleTiles/center': [],
+  'battleTiles/right': [],
+  totalFood: 0,
+  allHeroesJson
+};
+const state = Object.assign({}, initGameState);
+
+const getters = {
+  state(state) {
+    return state.gameState;
+  },
+  allHeroes(state) {
+    return state.allHeroes;
+  },
+  deckMode(state) {
+    return state.deckMode;
+  },
+  myColor(state, getters, rootState) {
+    let color = 'blue';
+    if (rootState.username === state.redPlayer.name) {
+      color = 'red';
+    }
+    return color;
+  },
+  ennemyColor(state, getters) {
+    let color = 'red';
+    if (getters.myColor === 'red') {
+      color = 'blue';
+    }
+    return color;
+  },
+  isItMyTurn(state, getters) {
+    return state['waitingFor/' + getters.myColor];
+  },
+  isPopularityRule(state) {
+    return state.advRules.includes('popularity');
+  },
+  isDiscardRule(state) {
+    return state.advRules.includes('discard');
+  },
+  heroById: state => id => {
+    return state.allHeroes.find(hero => hero.id === id);
+  },
+  myTwelveHeroes(state, getters) {
+    let myColor = getters.myColor;
+    let myHeroesIds = getters[myColor + 'Player/twelveHeroes'].map(
+      hero => hero.id
+    );
+    return state.allHeroes.filter(hero => {
+      return myHeroesIds.includes(hero.id);
+    });
+  },
+  myDraftIds(state, getters) {
+    let myColor = getters.myColor;
+    return getters[myColor + 'Player/draftIds'];
+  },
+  filterHeroes: state => filter => {
+    // filter has
+    //  - faction (orcs humans elves meca none all)
+    //  - popularity (with without)
+    //  - draft (yes no all)
+    //  -  minPower <= power <= maxPower
+    //  -  minCost <= cost <= maxCost
+    //  - byName : if true filter name with filter.name
+    //  - byId : [] of ids, deactivated if null or empty.
+    if (!filter.faction) filter.faction = 'all';
+    if (!filter.popularity) filter.popularity = 'without';
+    if (!filter.minPower) filter.minPower = 0;
+    if (!filter.maxPower) filter.maxPower = 0;
+    if (!filter.minCost) filter.minCost = 0;
+    if (!filter.maxCost) filter.minCost = 0;
+
+    let draftFilter = 'all';
+    switch (filter.draft) {
+      case 'yes':
+        draftFilter = true;
+        break;
+      case 'no':
+        draftFilter = false;
+        break;
+
+      default:
+        draftFilter = 'all';
+        break;
+    }
+    return state.allHeroes.filter(hero => {
+      if (filter.faction !== 'all' && hero.faction !== filter.faction)
+        return false;
+      if (
+        filter.popularity !== 'none' &&
+        hero.popularity !== 'any' &&
+        hero.popularity !== filter.popularity
+      )
+        return false;
+
+      if (draftFilter !== 'all' && hero.isDraftable !== draftFilter)
+        return false;
+
+      if (hero.power < filter.minPower || hero.power > filter.maxPower)
+        return false;
+      if (hero.cost < filter.minCost || hero.cost > filter.maxCost)
+        return false;
+
+      if (
+        filter.byName === true &&
+        !hero.name.toLowerCase().includes(filter.name.toLowerCase())
+      )
+        return false;
+
+      if (
+        filter.byId &&
+        filter.byId.length > 0 &&
+        !filter.byId.includes(hero.id)
+      ) {
+        return false;
+      }
+      return true;
+    });
+  }
+};
+
+const mutations = {
+  SET_HEROES: (state, payload) => {
+    state.allHeroes = payload;
+  },
+  SET_FULL_GAME_STATE: (state, payload) => {
+    Object.assign(state, payload);
+  },
+  INIT_GAME_STATE: state => {
+    Object.assign(state, initGameState);
+  },
+  SET_WAITING_FOR: (state, payload) => {
+    state['waitingFor/' + payload.color] = payload.value;
+  }
+};
+
+const actions = {
+  setHeroes: ({ commit }, payload) => {
+    commit('SET_HEROES', payload);
+  },
+  update: ({ commit }, payload) => {
+    commit('SET_FULL_GAME_STATE', payload.game);
+    commit('bluePlayer/SET_FULL_PLAYER_STATE', payload.bluePlayer);
+    commit('redPlayer/SET_FULL_PLAYER_STATE', payload.redPlayer);
+  },
+  resetGameState: ({ commit }) => {
+    commit('INIT_GAME_STATE');
+    commit('bluePlayer/INIT_PLAYER_STATE');
+    commit('redPlayer/INIT_PLAYER_STATE');
+  }
+};
+const bluePlayer = new AnyPlayer();
+const redPlayer = new AnyPlayer();
+export default {
+  namespaced: true,
+  state,
+  getters,
+  mutations,
+  actions,
+  modules: {
+    bluePlayer: {
+      namespaced: true,
+      ...bluePlayer
+    },
+    redPlayer: {
+      namespaced: true,
+      ...redPlayer
+    }
+  }
+};

+ 75 - 0
src/store/game/player/any-player.js

@@ -0,0 +1,75 @@
+import { socketService } from '../../../main';
+export default function AnyPlayer() {
+  const initPlayerState = {
+    // Username of this player
+    name: '',
+    // Color of this player (blue or red)
+    color: '',
+    // Faction chosen (will stay empty if not faction mode)
+    faction: '',
+    // Will contain the IDs of the heroes selectable for draft mode
+    draftHeroesIds: [],
+    // the 12 heroes the player is playing with
+    // Player will fill it himself if mode is draft or tournament
+    // contains object : {id:heroId , position:'pile' , possibleActions :[]}
+    twelveHeroes: [],
+    foodInCamp: 0,
+    // Can see already we might have issues with left/right depending from where we look :)
+    'foodInBattle/left': 0,
+    'foodInBattle/center': 0,
+    'foodInBattle/right': 0,
+    // During military phase it will contain actions made by player to be replayed by other
+    actionsPerformed: []
+  };
+
+  const state = Object.assign({}, initPlayerState);
+
+  const getters = {
+    color: state => {
+      return state.color;
+    },
+    draftIds: state => {
+      return state.draftHeroesIds;
+    },
+    twelveHeroes: state => {
+      return state.twelveHeroes;
+    }
+  };
+
+  const mutations = {
+    SET_FULL_PLAYER_STATE: (state, payload) => {
+      Object.assign(state, payload);
+    },
+    INIT_PLAYER_STATE: state => {
+      Object.assign(state, initPlayerState);
+    }
+  };
+
+  const actions = {
+    submitFaction: ({ commit, state }, payload) => {
+      commit(
+        'game/SET_WAITING_FOR',
+        { color: state.color, value: false },
+        { root: true }
+      );
+      socketService
+        .endTurn({ color: state.color, faction: payload })
+        .catch(err => {
+          console.log('Error sending data : ', err);
+          //Unvalid submit
+          commit(
+            'game/SET_WAITING_FOR',
+            { color: state.color, value: true },
+            { root: true }
+          );
+        });
+    }
+  };
+  return {
+    namespace: true,
+    state,
+    getters,
+    mutations,
+    actions
+  };
+}

+ 0 - 85
src/store/heroes/heroes.js

@@ -1,85 +0,0 @@
-import { initHeroesFromJson } from '../../server-shared/heroesHelper';
-import allHeroesJson from '../../server-shared/all-heroes.json';
-const state = {
-  heroes: new Set()
-};
-
-const getters = {
-  all(state) {
-    return state.heroes;
-  },
-  by: state => filter => {
-    // filter has
-    //  - faction (orcs humans elves meca none all)
-    //  - popularity (with without)
-    //  - draft (yes no all)
-    //  -  minPower <= power <= maxPower
-    //  -  minCost <= cost <= maxCost
-    //  - byName : if true filter name with filter.name
-    if (!filter.faction) filter.faction = 'all';
-    if (!filter.popularity) filter.popularity = 'without';
-    if (!filter.minPower) filter.minPower = 0;
-    if (!filter.maxPower) filter.maxPower = 0;
-    if (!filter.minCost) filter.minCost = 0;
-    if (!filter.maxCost) filter.minCost = 0;
-
-    let draftFilter = 'all';
-    switch (filter.draft) {
-      case 'yes':
-        draftFilter = true;
-        break;
-      case 'no':
-        draftFilter = false;
-        break;
-
-      default:
-        draftFilter = 'all';
-        break;
-    }
-
-    return Array.from(state.heroes).filter(hero => {
-      if (filter.faction !== 'all' && hero.faction !== filter.faction)
-        return false;
-      if (hero.popularity !== 'any' && hero.popularity !== filter.popularity)
-        return false;
-
-      if (draftFilter !== 'all' && hero.isDraftable !== draftFilter)
-        return false;
-
-      if (hero.power < filter.minPower || hero.power > filter.maxPower)
-        return false;
-      if (hero.cost < filter.minCost || hero.cost > filter.maxCost)
-        return false;
-
-      if (
-        filter.byName === true &&
-        !hero.name.toLowerCase().includes(filter.name.toLowerCase())
-      )
-        return false;
-
-      return true;
-    });
-  }
-};
-
-const mutations = {
-  SET_HEROES: (state, payload) => {
-    state.heroes = payload;
-  }
-};
-
-const actions = {
-  setFromLocalJson: ({ commit }) => {
-    commit('SET_HEROES', initHeroesFromJson(allHeroesJson));
-  },
-  setAvailableHeroes: ({ commit }, payload) => {
-    commit('SET_HEROES', payload);
-  }
-};
-export default {
-  namespaced: true,
-  state,
-  getters,
-  mutations,
-  actions
-};

+ 188 - 0
src/store/menu/menu.js

@@ -0,0 +1,188 @@
+import { socketService } from '../../main';
+import types from '../types';
+
+const state = {
+  joinableGames: [],
+  loadGamesStatus: { status: types.request.IDLE, text: 'Not connected' },
+  connectionStatus: { status: types.request.IDLE, text: 'Not connected' },
+  createGameStatus: { status: types.request.IDLE, text: 'Not connected' },
+  joinGameStatus: { status: types.request.IDLE, text: 'Not connected' }
+};
+
+const getters = {
+  joinableGames(state) {
+    return state.joinableGames;
+  },
+  connectionStatus(state) {
+    return state.connectionStatus;
+  },
+  loadGamesStatus(state) {
+    return state.loadGamesStatus;
+  },
+  createGameStatus(state) {
+    return state.createGameStatus;
+  },
+  joinGameStatus(state) {
+    return state.joinGameStatus;
+  }
+};
+
+const mutations = {
+  SET_JOINABLE_GAMES: (state, payload) => {
+    state.joinableGames = payload;
+  },
+  SET_CONNECTION_STATUS: (state, payload) => {
+    state.connectionStatus = payload;
+  },
+  SET_LOAD_GAMES_STATUS: (state, payload) => {
+    state.loadGamesStatus = payload;
+  },
+  SET_CREATE_GAME_STATUS: (state, payload) => {
+    state.createGameStatus = payload;
+  },
+  SET_JOIN_GAME_STATUS: (state, payload) => {
+    state.joinGameStatus = payload;
+  },
+  RESET_ALL_STATUS: state => {
+    let status = { status: types.request.IDLE, text: 'Not connected' };
+    state.connectionStatus = status;
+    state.loadGamesStatus = status;
+    state.createGameStatus = status;
+    state.joinGameStatus = status;
+  }
+};
+
+const actions = {
+  fetchJoinableGames: ({ commit, rootState }) => {
+    let loadGamesStatus = {
+      status: types.request.REQUESTED,
+      text: 'Loading Games'
+    };
+    commit('SET_LOAD_GAMES_STATUS', loadGamesStatus);
+    socketService
+      .getGamesList(rootState.username)
+      .then(games => {
+        loadGamesStatus.status = types.request.SUCCESS;
+        if (games.length === 0) {
+          loadGamesStatus.text =
+            'No games found, wait for a new or create one !';
+        } else {
+          loadGamesStatus.text = 'Online games loaded';
+        }
+        commit('SET_JOINABLE_GAMES', games);
+        commit('SET_LOAD_GAMES_STATUS', loadGamesStatus);
+      })
+      .catch(() => {
+        loadGamesStatus = {
+          status: types.request.ERROR,
+          text: 'Error reaching server'
+        };
+        commit('SET_LOAD_GAMES_STATUS', loadGamesStatus);
+      });
+  },
+  connect: ({ commit }, payload) => {
+    return new Promise((resolve, reject) => {
+      let connectionStatus = {};
+      if (payload === '') {
+        connectionStatus = {
+          status: types.request.ERROR,
+          text: 'Enter a valid username'
+        };
+
+        commit('SET_CONNECTION_STATUS', connectionStatus);
+        reject();
+      } else {
+        connectionStatus = {
+          status: types.request.REQUESTED,
+          text: 'Connecting...'
+        };
+        commit('SET_CONNECTION_STATUS', connectionStatus);
+        socketService
+          .connect(payload)
+          .then(() => {
+            connectionStatus = {
+              status: types.request.SUCCESS,
+              text: 'Connected as ' + payload
+            };
+            commit('SET_CONNECTION_STATUS', connectionStatus);
+            commit('SET_USERNAME', payload, { root: true });
+            commit('SET_IS_CONNECTED', true, { root: true });
+            localStorage.setItem('username', payload);
+            resolve();
+          })
+          .catch(err => {
+            connectionStatus = {
+              status: types.request.ERROR,
+              text: 'Could not connect as ' + payload + ' (' + err + ')'
+            };
+            commit('SET_CONNECTION_STATUS', connectionStatus);
+            reject();
+          });
+      }
+    });
+  },
+  createOnlineGame: ({ commit, rootState }, payload) => {
+    let createGameStatus = {
+      status: types.request.REQUESTED,
+      text: 'Creating Game'
+    };
+    let game = {
+      player1: rootState.username,
+      player2: '',
+      deck: payload.deckMode,
+      advRules: payload.advRules,
+      status: 'CREATED',
+      data: '{}'
+    };
+    commit('SET_CREATE_GAME_STATUS', createGameStatus);
+
+    socketService
+      .createGame(game)
+      .then(id => {
+        createGameStatus.status = types.request.SUCCESS;
+        createGameStatus.text = 'Success creating online game ' + id;
+        commit('SET_CREATE_GAME_STATUS', createGameStatus);
+        commit('SET_GAME_ID', id, { root: true });
+        commit('SET_IS_GAME_RUNNING', true, { root: true });
+      })
+      .catch(() => {
+        createGameStatus = {
+          status: types.request.ERROR,
+          text: 'Error reaching server'
+        };
+        commit('SET_CREATE_GAME_STATUS', createGameStatus);
+      });
+  },
+  joinOnlineGame: ({ commit, rootState }, payload) => {
+    let joinGameStatus = {
+      status: types.request.REQUESTED,
+      text: 'Joining Game'
+    };
+    commit('SET_JOIN_GAME_STATUS', joinGameStatus);
+
+    socketService
+      .joinGame(rootState.username, payload.id, payload.isNew)
+      .then(() => {
+        joinGameStatus.status = types.request.SUCCESS;
+        joinGameStatus.text = 'Success joining online game ' + payload.id;
+        commit('SET_JOIN_GAME_STATUS', joinGameStatus);
+        commit('SET_GAME_ID', payload.id, { root: true });
+        commit('SET_IS_GAME_RUNNING', true, { root: true });
+        if (!payload.isNew) {
+          localStorage.setItem('gameId', payload.id);
+        }
+      })
+      .catch(() => {
+        joinGameStatus.status = types.request.ERROR;
+        joinGameStatus.text = 'Error +-reaching server';
+        commit('SET_JOIN_GAME_STATUS', joinGameStatus);
+      });
+  }
+};
+export default {
+  namespaced: true,
+  state,
+  getters,
+  mutations,
+  actions
+};

+ 47 - 2
src/store/store.js

@@ -4,30 +4,74 @@ import Vuex from 'vuex';
 
 Vue.use(Vuex);
 
-import heroes from './heroes/heroes';
+import menu from './menu/menu';
+import game from './game/game';
+import { socketService } from '../main';
 
 const state = {
   username: '',
+  isConnected: false,
   gameId: -1,
+  isGameRunning: false,
   messages: []
 };
 
 const getters = {
   messages(state) {
     return state.messages;
+  },
+  gameId(state) {
+    return state.gameId;
+  },
+  isGameRunning(state) {
+    return state.isGameRunning;
+  },
+  username(state) {
+    return state.username;
+  },
+  isConnected(state) {
+    return state.isConnected;
   }
 };
 
 const mutations = {
   ADD_MESSAGE: (state, payload) => {
     state.messages.push(payload);
+  },
+  SET_GAME_ID: (state, payload) => {
+    state.gameId = payload;
+  },
+  SET_IS_GAME_RUNNING: (state, payload) => {
+    state.isGameRunning = payload;
+  },
+  SET_USERNAME: (state, payload) => {
+    state.username = payload;
+  },
+  SET_IS_CONNECTED: (state, payload) => {
+    state.isConnected = payload;
   }
 };
 const actions = {
   addMessageToQueue: ({ commit }, payload) => {
     commit('ADD_MESSAGE', payload);
+  },
+  stopGame: ({ commit, rootState }) => {
+    socketService.leaveGame(rootState.username);
+    commit('SET_IS_GAME_RUNNING', false);
+    localStorage.removeItem('gameId');
+  },
+  disconnect: ({ commit }) => {
+    socketService.disconnect();
+    commit('SET_GAME_ID', -1);
+    commit('SET_IS_GAME_RUNNING', false);
+    commit('SET_USERNAME', '');
+    commit('SET_IS_CONNECTED', false);
+    commit('menu/RESET_ALL_STATUS');
+    localStorage.removeItem('username');
+    localStorage.removeItem('gameId');
   }
 };
+
 export const store = new Vuex.Store({
   state,
   getters,
@@ -35,6 +79,7 @@ export const store = new Vuex.Store({
   actions,
 
   modules: {
-    heroes
+    menu,
+    game
   }
 });

+ 39 - 0
src/store/types.js

@@ -0,0 +1,39 @@
+'use strict';
+
+const Constants = {
+  //  Server requests status
+  request: {
+    IDLE: 'idle',
+    REQUESTED: 'requested',
+    SUCCESS: 'success',
+    ERROR: 'ERROR'
+  },
+
+  // Positions of heroes in game
+  POS_PILE: 'pile',
+  POS_HAND: 'hand',
+  POS_DISCARD: 'discard',
+  POS_CAMP: 'camp',
+  POS_BATTLE_LEFT: 'battle_left',
+  POS_BATTLE_CENTER: 'battle_center',
+  POS_BATTLE_RIGHT: 'battle_right',
+
+  // Possible hero actions in game
+  HERO_RECRUIT: 'recruit',
+  HERO_DEPLOY: 'deploy',
+  HERO_MOVE: 'move',
+  HERO_ABILITY: 'ability',
+  HERO_DISMISS: 'dismiss',
+  HERO_DISCARD: 'discard',
+
+  // Possible actions for player in game
+  PLAYER_SUPPLY: 'supply'
+};
+
+Constants.install = function(Vue) {
+  Vue.prototype.$types = key => {
+    return Constants[key];
+  };
+};
+
+export default Constants;

Some files were not shown because too many files changed in this diff