├── src
├── store
│ ├── actions
│ │ ├── index.js
│ │ └── game
│ │ │ └── index.js
│ ├── reducers
│ │ ├── index.js
│ │ ├── ui.js
│ │ └── game.js
│ └── index.js
├── config
│ └── index.js
├── components
│ ├── views
│ │ ├── index.js
│ │ ├── Main.js
│ │ ├── Result.js
│ │ └── Battlefield.js
│ ├── battlefield
│ │ ├── index.js
│ │ ├── Hand.js
│ │ ├── Slots.js
│ │ ├── Slot.js
│ │ └── PlayerStatus.js
│ └── cards
│ │ ├── styles.js
│ │ ├── elements
│ │ ├── Icon.css
│ │ └── Icon.js
│ │ └── Card.js
├── index.css
├── App.css
├── libs
│ ├── models
│ │ ├── index.js
│ │ ├── types.js
│ │ ├── deck.js
│ │ ├── card.js
│ │ ├── ai.js
│ │ ├── player.js
│ │ └── battlefield.js
│ ├── utils
│ │ └── index.js
│ └── generators
│ │ └── cardGenerator.js
├── __tests__
│ └── libs
│ │ └── models
│ │ ├── deck.test.js
│ │ ├── card.test.js
│ │ └── battlefield.test.js
├── index.js
├── App.js
└── registerServiceWorker.js
├── public
├── favicon.ico
├── manifest.json
├── assets
│ └── img
│ │ ├── elements
│ │ ├── earth.svg
│ │ ├── water.svg
│ │ └── fire.svg
│ │ └── unknown.svg
└── index.html
├── jsconfig.json
├── .travis.yml
├── .gitignore
├── README.md
└── package.json
/src/store/actions/index.js:
--------------------------------------------------------------------------------
1 | export * from './game'
--------------------------------------------------------------------------------
/src/config/index.js:
--------------------------------------------------------------------------------
1 | export const CARDS_IN_DECK = 25;
--------------------------------------------------------------------------------
/src/store/reducers/index.js:
--------------------------------------------------------------------------------
1 | export * from './game';
2 | export * from './ui';
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nientedidecente/elime/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/components/views/index.js:
--------------------------------------------------------------------------------
1 | export * from './Main';
2 | export * from './Battlefield';
3 |
--------------------------------------------------------------------------------
/src/components/battlefield/index.js:
--------------------------------------------------------------------------------
1 | export * from './Slots';
2 | export * from './PlayerStatus';
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .centeredContent {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | }
--------------------------------------------------------------------------------
/src/components/cards/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | cardWrapper: {margin: 'auto', height: '150px', width: '100px'}
3 | };
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "paths": {
5 | "@/*":["src/*"]
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/src/components/cards/elements/Icon.css:
--------------------------------------------------------------------------------
1 | .elementIcon {
2 | width: 30px;
3 | align-self: center;
4 | margin-bottom: 25px;
5 | margin-top: 25px;
6 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | notifications:
2 | email: false
3 | language: node_js
4 | node_js:
5 | - 8
6 | cache: yarn
7 |
8 | before_script:
9 | - yarn install
10 |
11 | script:
12 | - yarn test
--------------------------------------------------------------------------------
/src/libs/models/index.js:
--------------------------------------------------------------------------------
1 | export * from './card';
2 | export * from './deck';
3 | export * from './battlefield';
4 | export * from './player';
5 | export * from './types';
6 | export * from './ai';
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/src/libs/utils/index.js:
--------------------------------------------------------------------------------
1 | export const arrayShuffle = array => array.map(a => [Math.random(), a]).sort((a, b) => a[0] - b[0]).map(a => a[1]);
2 | export const cloneObject = (className, object) => {
3 | const clone = Object.assign({}, object);
4 | Object.setPrototypeOf(clone, className.prototype);
5 | return clone;
6 | };
--------------------------------------------------------------------------------
/src/__tests__/libs/models/deck.test.js:
--------------------------------------------------------------------------------
1 | import {Deck} from "../../../libs/models";
2 | import {cardGenerator} from "../../../libs/generators/cardGenerator";
3 |
4 | test('a deck can be instantiated correctly', () => {
5 | const card = new Deck(
6 | cardGenerator.generate()
7 | );
8 | expect(card).toBeTruthy();
9 | });
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import 'semantic-ui-css/semantic.min.css';
5 | import App from './App';
6 | import registerServiceWorker from './registerServiceWorker';
7 |
8 | ReactDOM.render(, document.getElementById('root'));
9 | registerServiceWorker();
10 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware, compose, combineReducers} from 'redux';
2 | import thunk from 'redux-thunk';
3 |
4 | import {game, ui} from './reducers';
5 |
6 |
7 | const reducers = combineReducers({
8 | game,
9 | ui
10 | });
11 |
12 |
13 | const middlewares = [thunk];
14 | export const store = compose(
15 | applyMiddleware(...middlewares)
16 | )(createStore)(reducers);
--------------------------------------------------------------------------------
/src/components/battlefield/Hand.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Segment } from "semantic-ui-react";
3 | import { Slot } from "./Slot";
4 |
5 | const Hand = ({ hand, onSelect, onClose }) => (
6 |
7 | {
8 | hand.map((c, i) => (
9 | onSelect(c)} />
10 | ))
11 | }
12 |
13 |
14 | );
15 |
16 |
17 | export { Hand };
--------------------------------------------------------------------------------
/src/components/cards/elements/Icon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Icon.css';
3 | import { TYPES } from "libs/models";
4 |
5 | const elementsMapping = {
6 | [TYPES.WATER]: 'assets/img/elements/water.svg',
7 | [TYPES.FIRE]: 'assets/img/elements/fire.svg',
8 | [TYPES.EARTH]: 'assets/img/elements/earth.svg',
9 | default: 'assets/img/unknown.svg'
10 | };
11 |
12 | const Icon = ({ element }) =>
;
13 | export { Icon };
--------------------------------------------------------------------------------
/src/store/reducers/ui.js:
--------------------------------------------------------------------------------
1 | import {ERROR_MESSAGE, CLEAR_MESSAGE} from "../actions/game";
2 |
3 | const initialState = {
4 | message: null,
5 | error: false
6 | };
7 |
8 | export const ui = (state = initialState, action) => {
9 | switch (action.type) {
10 | case CLEAR_MESSAGE:
11 | case ERROR_MESSAGE: {
12 | return {
13 | ...state,
14 | ...action.data
15 | }
16 | }
17 | default: {
18 | return state;
19 | }
20 | }
21 | };
--------------------------------------------------------------------------------
/src/libs/models/types.js:
--------------------------------------------------------------------------------
1 | export const TYPES = {
2 | EARTH: 'earth',
3 | FIRE: 'fire',
4 | WATER: 'water'
5 | };
6 |
7 | export const RESOLVE_MATRIX = {
8 | [TYPES.EARTH]: {
9 | [TYPES.EARTH]: 0,
10 | [TYPES.FIRE]: -1,
11 | [TYPES.WATER]: 1
12 | },
13 | [TYPES.FIRE]: {
14 | [TYPES.EARTH]: 1,
15 | [TYPES.FIRE]: 0,
16 | [TYPES.WATER]: -1
17 | },
18 | [TYPES.WATER]: {
19 | [TYPES.EARTH]: -1,
20 | [TYPES.FIRE]: 1,
21 | [TYPES.WATER]: 0
22 | }
23 | };
--------------------------------------------------------------------------------
/src/__tests__/libs/models/card.test.js:
--------------------------------------------------------------------------------
1 | import { Card, TYPES } from "../../../libs/models";
2 | import { cardGenerator } from "../../../libs/generators/cardGenerator";
3 |
4 | test('a card can be instantiated correctly', () => {
5 | const card = new Card({ name: '', type: TYPES.EARTH });
6 | expect(card).toBeTruthy();
7 | });
8 |
9 | test('generator forces fields', () => {
10 | const card = cardGenerator.generateOne({ type: TYPES.FIRE, name: 'banana' });
11 | expect(card.type).toBe(TYPES.FIRE);
12 | expect(card.name).toBe('banana');
13 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # elime
2 |
3 | [](https://travis-ci.org/vikkio88/elime)
4 |
5 | An opensource clone of [Earthcore: Shattered Elements](http://www.earthcoregame.com/).
6 |
7 | On the 31st of March 2018, this game was officially abandoned, so it is not possible to play it anymore, not even single player campaign, as the app will try to connect to a non-existing server.
8 |
9 | I am going to make a small game engine that will use the same in game rules, using es6 and react.
10 |
11 | Any help would be appreciated.
12 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Provider } from 'react-redux';
3 | import { Main } from "./components/views";
4 | import './App.css';
5 |
6 | import { store } from 'store';
7 | import { initGame } from "store/actions/game";
8 |
9 | class App extends Component {
10 |
11 | componentWillMount() {
12 | store.dispatch(initGame())
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/src/store/reducers/game.js:
--------------------------------------------------------------------------------
1 | import {DESELECT_CARD, FINISHED, SELECT_CARD, UPDATE_BATTLEFIELD} from "../actions/game";
2 |
3 | const initialState = {
4 | battlefield: null,
5 | selectedCard: null,
6 | finished: false
7 | };
8 |
9 | export const game = (state = initialState, action) => {
10 | switch (action.type) {
11 | case DESELECT_CARD:
12 | case SELECT_CARD:
13 | case FINISHED:
14 | case UPDATE_BATTLEFIELD: {
15 | return {
16 | ...state,
17 | ...action.data
18 | }
19 | }
20 | default:
21 | return state;
22 | }
23 | };
--------------------------------------------------------------------------------
/src/libs/models/deck.js:
--------------------------------------------------------------------------------
1 | import {arrayShuffle} from "../utils";
2 | import {Card} from './card';
3 |
4 | export class Deck {
5 | constructor(cards = []) {
6 | this.cards = cards;
7 | }
8 |
9 | shuffle() {
10 | this.cards = arrayShuffle(this.cards);
11 | }
12 |
13 | cardLeft() {
14 | return this.cards.length;
15 | }
16 |
17 | draw() {
18 | return this.cards.pop();
19 | }
20 |
21 | toJs() {
22 | return {
23 | cards: this.cards.map(c => c.toJs())
24 | }
25 | }
26 |
27 | static fromJs(jsObject) {
28 | const deck = new Deck();
29 | deck.cards = jsObject.cards.map(c => Card.fromJs(c));
30 | return deck;
31 | }
32 | }
--------------------------------------------------------------------------------
/src/libs/generators/cardGenerator.js:
--------------------------------------------------------------------------------
1 | import {randomizer, range} from 'uvk';
2 | import {Card, CARD_COSTS, TYPES} from "../models";
3 | import {CARDS_IN_DECK} from "../../config";
4 |
5 | export const cardGenerator = {
6 | generateOne(forcedFields = {}) {
7 | const type = randomizer.pickOne(Object.values(TYPES));
8 | const cost = randomizer.int(CARD_COSTS.LOW, CARD_COSTS.HIGH);
9 | const name = `${type}_${cost}`;
10 | return new Card({
11 | name,
12 | cost,
13 | type,
14 | ...forcedFields
15 | });
16 | },
17 |
18 | generate(number = CARDS_IN_DECK, forcedField = {}) {
19 | return range(number).map(() => this.generateOne(forcedField));
20 | }
21 | };
--------------------------------------------------------------------------------
/src/libs/models/card.js:
--------------------------------------------------------------------------------
1 | export const CARD_COSTS = {
2 | LOW: 1,
3 | HIGH: 10
4 | };
5 |
6 | export class Card {
7 | constructor({name, type, cost, action = null}) {
8 | this.name = name;
9 | this.type = type;
10 | this.cost = cost;
11 | this.action = action;
12 | }
13 |
14 | toJs() {
15 | return {
16 | name: this.name,
17 | type: this.type,
18 | cost: this.cost,
19 | action: this.action
20 | }
21 | }
22 |
23 | static fromJs(jsObject) {
24 | const card = new Card({});
25 | card.name = jsObject.name;
26 | card.type = jsObject.type;
27 | card.cost = jsObject.cost;
28 | card.action = jsObject.action;
29 | return card;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/libs/models/ai.js:
--------------------------------------------------------------------------------
1 | import {randomizer} from 'uvk';
2 | import {SLOTS} from "./battlefield";
3 |
4 | class Ai {
5 | battlefield = null;
6 | playerId = null;
7 |
8 | constructor(battlefield, playerId) {
9 | this.battlefield = battlefield;
10 | this.playerId = playerId
11 | }
12 |
13 | getSelf() {
14 | return this.battlefield.players[this.playerId];
15 | }
16 |
17 | play() {
18 | const me = this.getSelf();
19 | const card = randomizer.pickOne(me.hand);
20 | let slot = randomizer.pickOne(Object.values(SLOTS));
21 | while (!this.battlefield.isMoveValid(me.id, card, slot)) {
22 | slot = randomizer.pickOne(Object.values(SLOTS));
23 | }
24 | return {player: me.id, card, slot};
25 | }
26 | }
27 |
28 | export {Ai};
--------------------------------------------------------------------------------
/src/components/views/Main.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Container } from "semantic-ui-react";
4 | import { Battlefield } from "./Battlefield";
5 | import { Result } from "./Result";
6 |
7 | class MainView extends Component {
8 | render() {
9 | const { finished } = this.props;
10 | return (
11 |
12 | {!finished && }
13 | {finished && }
14 |
15 | );
16 | }
17 | }
18 |
19 | const stateToProps = ({ game }) => {
20 | const { finished } = game;
21 | return { finished };
22 | };
23 | const dispatchToProps = dispatch => {
24 | return {};
25 | };
26 | const Main = connect(stateToProps, dispatchToProps)(MainView);
27 | export { Main };
--------------------------------------------------------------------------------
/src/components/battlefield/Slots.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Slot } from "./Slot";
3 | import { Grid } from "semantic-ui-react";
4 |
5 | class Slots extends Component {
6 | render() {
7 | const { slots, selectable, playerId, cumulative } = this.props;
8 | return (
9 |
10 |
11 | {Object.keys(slots).map(k => (
12 |
13 |
20 |
21 | ))}
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export { Slots };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elime",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.2.0",
7 | "react-dom": "^16.2.0",
8 | "react-redux": "^5.0.7",
9 | "redux": "^3.7.2",
10 | "redux-thunk": "^2.2.0",
11 | "semantic-ui-css": "^2.3.1",
12 | "semantic-ui-react": "^2.1.3",
13 | "uvk": "^1.0.2"
14 | },
15 | "devDependencies": {
16 | "react-scripts": "^5.0.1"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "buildDeploy": "GENERATE_SOURCEMAP=false react-scripts build && npm run deploy",
22 | "test": "react-scripts test --env=jsdom",
23 | "eject": "react-scripts eject",
24 | "deploy": "surge --domain elime.surge.sh build/"
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/public/assets/img/elements/earth.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
44 |
--------------------------------------------------------------------------------
/src/components/cards/Card.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Card as SCard } from 'semantic-ui-react';
3 | import { Icon } from "./elements/Icon";
4 | import { TYPES } from "libs/models";
5 |
6 | import styles from './styles';
7 |
8 | const colourMapping = {
9 | [TYPES.WATER]: 'blue',
10 | [TYPES.FIRE]: 'red',
11 | [TYPES.EARTH]: 'green',
12 | };
13 |
14 | class Card extends Component {
15 | render() {
16 | const { card, covered, onClick } = this.props;
17 | if (covered) {
18 | return (
19 |
20 |
21 |
22 | );
23 | }
24 | return (
25 | onClick() : null}
29 | >
30 |
31 |
32 |
33 | {card.cost}
34 |
35 |
36 | {card.name}
37 |
38 |
39 |
40 | );
41 | }
42 | }
43 |
44 | export { Card };
--------------------------------------------------------------------------------
/public/assets/img/elements/water.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
46 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Elime
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/battlefield/Slot.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Card } from "../cards/Card";
4 | import { Segment, Card as SCard, Label } from "semantic-ui-react";
5 | import styles from "../cards/styles";
6 | import { playCard } from "store/actions/game";
7 |
8 | class SlotView extends Component {
9 | selectSlot() {
10 | const { playerId, id: slotId, selectedCard, selectable, battlefield } = this.props;
11 | if (selectedCard && selectable) {
12 | this.props.playCard(battlefield, playerId, selectedCard, slotId)
13 | }
14 | }
15 |
16 | render() {
17 | const { card, onClick, selectable, cumulative } = this.props;
18 | return (
19 |
20 | {cumulative > 0 && }
21 | {card && onClick() : null} />}
22 | {!card &&
23 | this.selectSlot() : null}
26 | raised={selectable}
27 | />
28 | }
29 |
30 | );
31 | }
32 | }
33 |
34 |
35 | const stateToProps = ({ game }) => {
36 | const { selectedCard, battlefield } = game;
37 | return { selectedCard, battlefield };
38 | };
39 | const dispatchToProps = dispatch => {
40 | return {
41 | playCard(battlefield, playerId, card, slotId) {
42 | dispatch(playCard(battlefield, playerId, card, slotId));
43 | }
44 | };
45 | };
46 |
47 | const Slot = connect(stateToProps, dispatchToProps)(SlotView);
48 | export { Slot };
--------------------------------------------------------------------------------
/public/assets/img/elements/fire.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
50 |
--------------------------------------------------------------------------------
/src/components/views/Result.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Statistic, Icon, Button } from "semantic-ui-react";
4 | import Header from "semantic-ui-react/dist/es/elements/Header/Header";
5 | import { PLAYERS } from "libs/models";
6 | import { initGame } from "store/actions/game";
7 |
8 | class ResultView extends Component {
9 | render() {
10 | const { result, initGame } = this.props;
11 | return (
12 |
19 |
20 |
21 |
22 | {PLAYERS.ONE === result.winner ? 'YOU WIN!' : 'YOU LOSE!'}
23 |
24 |
25 |
26 |
27 |
28 | {result[PLAYERS.ONE].life}
29 |
30 |
31 | Human life points
32 |
33 |
34 |
35 |
36 |
37 | {result[PLAYERS.TWO].life}
38 |
39 |
40 | CPU life points
41 |
42 |
43 |
44 |
45 |
53 |
54 |
55 | );
56 | }
57 | }
58 |
59 |
60 | const stateToProps = () => {
61 | return {};
62 | };
63 | const dispatchToProps = dispatch => {
64 | return {
65 | initGame() {
66 | dispatch(initGame({ randomStarter: true }));
67 | }
68 | };
69 | };
70 | const Result = connect(stateToProps, dispatchToProps)(ResultView);
71 |
72 | export { Result };
--------------------------------------------------------------------------------
/src/components/views/Battlefield.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { PlayerStatus, Slots } from "../battlefield";
3 | import { Grid, Message } from "semantic-ui-react";
4 | import { connect } from "react-redux";
5 | import { clearMessage, playAiTurn } from "store/actions/game";
6 |
7 | class BattlefieldView extends Component {
8 | render() {
9 | const { battlefield, ui, dismissMessage, playAiTurn } = this.props;
10 | const { player2: cpu, player1: human } = battlefield.status();
11 | const isFull = battlefield.isFull();
12 | const turn = !isFull ? battlefield.getTurn() : null;
13 |
14 | console.log('turn', turn);
15 | console.log('isFull?', isFull);
16 |
17 | if (turn === cpu.id) {
18 | playAiTurn(battlefield, cpu.id);
19 | }
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {ui.message && (
33 |
34 |
35 | dismissMessage()}>{ui.message}
36 |
37 |
38 | )}
39 |
40 |
41 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
55 | const stateToProps = ({ game, ui }) => {
56 | const { battlefield } = game;
57 | return { battlefield, ui };
58 | };
59 | const dispatchToProps = dispatch => {
60 | return {
61 | dismissMessage() {
62 | dispatch(clearMessage());
63 | },
64 | playAiTurn(battlefield, id) {
65 | dispatch(playAiTurn(battlefield, id));
66 | }
67 | };
68 | };
69 | const Battlefield = connect(stateToProps, dispatchToProps)(BattlefieldView);
70 | export { Battlefield };
--------------------------------------------------------------------------------
/public/assets/img/unknown.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
60 |
--------------------------------------------------------------------------------
/src/libs/models/player.js:
--------------------------------------------------------------------------------
1 | import {range} from 'uvk';
2 | import {Card} from './card';
3 | import {Deck} from './deck';
4 | import {SLOTS} from "./battlefield";
5 |
6 | const FALLBACK_LIFE = 20;
7 | const CARD_IN_HAND = 4;
8 |
9 | class Player {
10 | name = null;
11 | deck = null;
12 | hand = [];
13 | discardPile = [];
14 | life = FALLBACK_LIFE;
15 | slots = {
16 | [SLOTS.LEFT]: null,
17 | [SLOTS.CENTER]: null,
18 | [SLOTS.RIGHT]: null,
19 | };
20 |
21 | constructor(id, {name, deck, life = null}) {
22 | this.id = id;
23 | this.name = name;
24 | this.deck = deck;
25 | this.life = life || FALLBACK_LIFE;
26 | }
27 |
28 | status() {
29 | return {
30 | id: this.id,
31 | name: this.name,
32 | life: this.life,
33 | hand: this.hand.length,
34 | deck: this.deck.cardLeft(),
35 | slots: this.slots
36 | }
37 | }
38 |
39 | toHand(card) {
40 | if (this.hand.length < CARD_IN_HAND) {
41 | this.hand.push(card);
42 | }
43 | }
44 |
45 | fromHand(position) {
46 | const element = this.hand[position];
47 | this.hand = this.hand.filter((e, i) => i !== position);
48 | return element;
49 | }
50 |
51 | setHand() {
52 | range(CARD_IN_HAND - this.hand.length).forEach(() => {
53 | this.hand.push(this.deck.draw());
54 | });
55 | }
56 |
57 | toDiscard(card) {
58 | this.discardPile.push(card);
59 | }
60 |
61 | play(card, slot) {
62 | this.hand = this.hand.filter(c => c !== card);
63 | this.slots[slot] = card;
64 | }
65 |
66 | toJs() {
67 | return {
68 | id: this.id,
69 | name: this.name,
70 | life: this.life,
71 | slots: {
72 | [SLOTS.LEFT]: this.slots[SLOTS.LEFT] ? this.slots[SLOTS.LEFT].toJs() : null,
73 | [SLOTS.CENTER]: this.slots[SLOTS.CENTER] ? this.slots[SLOTS.CENTER].toJs() : null,
74 | [SLOTS.RIGHT]: this.slots[SLOTS.RIGHT] ? this.slots[SLOTS.RIGHT].toJs() : null
75 | },
76 | hand: this.hand.map(c => c.toJs()),
77 | deck: this.deck.toJs(),
78 | discardPile: this.discardPile.map(c => c.toJs())
79 | }
80 | }
81 |
82 | static fromJs(jsObject) {
83 | const player = new Player(null, {});
84 |
85 | player.id = jsObject.id;
86 | player.name = jsObject.name;
87 | player.life = jsObject.life;
88 | player.slots = {
89 | [SLOTS.LEFT]: jsObject.slots[SLOTS.LEFT] ? Card.fromJs(jsObject.slots[SLOTS.LEFT]) : null,
90 | [SLOTS.CENTER]: jsObject.slots[SLOTS.CENTER] ? Card.fromJs(jsObject.slots[SLOTS.CENTER]) : null,
91 | [SLOTS.RIGHT]: jsObject.slots[SLOTS.RIGHT] ? Card.fromJs(jsObject.slots[SLOTS.RIGHT]) : null
92 | };
93 | player.hand = jsObject.hand.map(c => Card.fromJs(c));
94 | player.deck = Deck.fromJs(jsObject.deck);
95 | player.discardPile = jsObject.discardPile.map(c => Card.fromJs(c));
96 | return player;
97 | }
98 | }
99 |
100 | export {Player}
--------------------------------------------------------------------------------
/src/store/actions/game/index.js:
--------------------------------------------------------------------------------
1 | import {randomizer} from 'uvk';
2 | import {Ai, BattleField, Deck, PLAYERS} from "../../../libs/models";
3 | import {cardGenerator} from "../../../libs/generators/cardGenerator";
4 | import {cloneObject} from "../../../libs/utils";
5 |
6 | export const UPDATE_BATTLEFIELD = 'update_battlefield';
7 | export const SELECT_CARD = 'select_card';
8 | export const DESELECT_CARD = 'deselect_card';
9 |
10 | export const FINISHED = 'finished';
11 |
12 | export const CLEAR_MESSAGE = 'clear_message';
13 | export const ERROR_MESSAGE = 'error_message';
14 |
15 |
16 | export const initGame = ({randomStarter = false} = {}) => {
17 | const playersDeck = new Deck(cardGenerator.generate());
18 | const cpusDeck = new Deck(cardGenerator.generate());
19 | const player = {name: 'Human', life: 20, deck: playersDeck};
20 | const cpu = {name: 'Computer', life: 20, deck: cpusDeck};
21 | const battlefield = new BattleField(player, cpu);
22 | battlefield.forceTurn(randomStarter ? randomizer.pickOne(Object.values(PLAYERS)) : PLAYERS.ONE);
23 |
24 | battlefield.setHand(PLAYERS.ONE);
25 | battlefield.setHand(PLAYERS.TWO);
26 |
27 | return {
28 | type: UPDATE_BATTLEFIELD,
29 | data: {
30 | battlefield,
31 | finished: false
32 | }
33 | }
34 | };
35 |
36 | export const selectCard = selectedCard => {
37 | return {
38 | type: SELECT_CARD,
39 | data: {
40 | selectedCard
41 | }
42 | }
43 | };
44 |
45 | export const deselectCard = () => {
46 | return {
47 | type: DESELECT_CARD,
48 | data: {
49 | selectedCard: null
50 | }
51 | }
52 | };
53 |
54 | export const playAiTurn = (battlefield, id) => {
55 | return dispatch => {
56 | const ai = new Ai(battlefield, id);
57 | const move = ai.play();
58 | setTimeout(() => dispatch(playCard(battlefield, move.player, move.card, move.slot)), 1000);
59 | };
60 | };
61 |
62 | export const playCard = (battlefield, playerId, card, slot) => {
63 | return dispatch => {
64 | const isMoveValid = battlefield.playCard(playerId, card, slot);
65 | console.log(playerId, card, slot);
66 | if (isMoveValid) {
67 | dispatch(updateBattleField(battlefield));
68 | if (battlefield.isFull()) {
69 | setTimeout(() => dispatch(resolve(battlefield)), 1000);
70 | }
71 | } else {
72 | dispatch(error('Cannot play that card in this slot, try another one'))
73 | }
74 | }
75 | };
76 |
77 | export const resolve = battlefield => {
78 | return dispatch => {
79 | battlefield.resolve();
80 | battlefield.setHands();
81 | battlefield.toggleTurn();
82 | if (battlefield.isOver()) {
83 | dispatch(finished(battlefield.result()))
84 | }
85 | dispatch(updateBattleField(battlefield))
86 | }
87 | };
88 |
89 |
90 | export const updateBattleField = battlefield => {
91 | return {
92 | type: UPDATE_BATTLEFIELD,
93 | data: {
94 | battlefield: cloneObject(BattleField, battlefield),
95 | selectedCard: null
96 | }
97 | }
98 | };
99 |
100 |
101 | export const error = message => {
102 | return {
103 | type: ERROR_MESSAGE,
104 | data: {
105 | message: message,
106 | error: true
107 | }
108 | }
109 | };
110 |
111 | export const finished = result => {
112 | return {
113 | type: FINISHED,
114 | data: {
115 | finished: result
116 | }
117 | }
118 | };
119 |
120 | export const clearMessage = () => {
121 | return {
122 | type: CLEAR_MESSAGE,
123 | data: {
124 | message: null,
125 | error: false
126 | }
127 | }
128 | };
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/battlefield/PlayerStatus.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Button, Header, Segment } from "semantic-ui-react";
3 | import { connect } from "react-redux";
4 | import { deselectCard, selectCard } from "store/actions/game";
5 | import { Card } from "../cards/Card";
6 | import { Hand } from "./Hand";
7 |
8 | class PlayerStatusView extends Component {
9 | state = {
10 | handShown: false
11 | };
12 |
13 | showHand() {
14 | this.setState({ handShown: true })
15 | }
16 |
17 | closeHand() {
18 | this.setState({ handShown: false })
19 | }
20 |
21 | selectCard(card) {
22 | this.closeHand();
23 | this.props.selectCard(card);
24 | }
25 |
26 | render() {
27 | const { player, playersTurn, local, battlefield, selectedCard, deselectCard } = this.props;
28 | const { handShown } = this.state;
29 | const canPlay = selectedCard === null && local;
30 |
31 | return (
32 |
33 | {handShown && (
34 | this.selectCard(c)}
37 | onClose={() => this.closeHand()}
38 | />
39 | )}
40 | {(local && selectedCard) && (
41 |
42 |
43 |
44 |
45 |
46 |
47 | )}
48 | {(!local || (!handShown && !selectedCard)) && (
49 |
50 |
55 | {player.name}
56 |
57 |
58 |
59 | {player.life}
60 |
61 | Health points
62 |
63 |
64 |
65 |
66 |
67 |
68 | {canPlay && (
69 |
70 | {player.hand}
71 |
72 |
75 |
76 | )
77 | }
78 | {!canPlay && (
79 |
80 | {player.hand}
81 |
82 | Hand
83 |
84 |
85 | )}
86 |
87 |
88 |
89 |
90 | {player.deck}
91 |
92 | Deck
93 |
94 |
95 |
96 |
97 |
98 |
99 | )}
100 |
101 | );
102 | }
103 | }
104 |
105 | const stateToProps = ({ game }) => {
106 | const { battlefield, selectedCard } = game;
107 | return { battlefield, selectedCard };
108 | };
109 | const dispatchToProps = dispatch => {
110 | return {
111 | selectCard(card) {
112 | dispatch(selectCard(card));
113 | },
114 | deselectCard() {
115 | dispatch(deselectCard());
116 | }
117 | };
118 | };
119 | const PlayerStatus = connect(stateToProps, dispatchToProps)(PlayerStatusView);
120 |
121 | export { PlayerStatus };
--------------------------------------------------------------------------------
/src/__tests__/libs/models/battlefield.test.js:
--------------------------------------------------------------------------------
1 | import { BattleField, Card, Deck, PLAYERS, SLOTS, TYPES } from "../../../libs/models";
2 | import { cardGenerator } from "../../../libs/generators/cardGenerator";
3 | import { CARDS_IN_DECK } from "../../../config";
4 |
5 | test('battle round test', () => {
6 | const cards = cardGenerator.generate(CARDS_IN_DECK);
7 | const deck = new Deck(cards);
8 | expect(deck.cards.length).toBe(CARDS_IN_DECK);
9 |
10 | deck.shuffle();
11 |
12 | const playerOne = { name: 'uno', deck };
13 | const playerTwo = { name: 'due', deck };
14 |
15 | const battleField = new BattleField(playerOne, playerTwo);
16 | battleField.forceTurn(PLAYERS.ONE);
17 | battleField.playCard(PLAYERS.ONE, deck.draw(), SLOTS.LEFT);
18 | battleField.playCard(PLAYERS.TWO, deck.draw(), SLOTS.CENTER);
19 | expect(battleField.isFull()).toBe(false);
20 |
21 | battleField.playCard(PLAYERS.ONE, deck.draw(), SLOTS.CENTER);
22 | battleField.playCard(PLAYERS.TWO, deck.draw(), SLOTS.LEFT);
23 | expect(battleField.isFull()).toBe(false);
24 |
25 | battleField.playCard(PLAYERS.ONE, deck.draw(), SLOTS.RIGHT);
26 | battleField.playCard(PLAYERS.TWO, deck.draw(), SLOTS.RIGHT);
27 | expect(battleField.isFull()).toBe(true);
28 | });
29 |
30 |
31 | test('if draw, cost will be carried next turn', () => {
32 | const cards = cardGenerator.generate(CARDS_IN_DECK);
33 | const deck = new Deck(cards);
34 | const playerOne = { name: 'uno', deck };
35 | const playerTwo = { name: 'due', deck };
36 | const cost = 10;
37 | const battleField = new BattleField(playerOne, playerTwo);
38 | const fire = new Card({ name: '', type: TYPES.FIRE, cost });
39 | const water = new Card({ name: '', type: TYPES.WATER, cost });
40 | expect(battleField.players[PLAYERS.ONE].discardPile.length).toBe(0);
41 | expect(battleField.players[PLAYERS.TWO].discardPile.length).toBe(0);
42 | battleField.forceTurn(PLAYERS.ONE);
43 | battleField.playCard(PLAYERS.ONE, fire, SLOTS.LEFT);
44 | battleField.playCard(PLAYERS.TWO, fire, SLOTS.CENTER);
45 | battleField.playCard(PLAYERS.ONE, fire, SLOTS.CENTER);
46 | battleField.playCard(PLAYERS.TWO, fire, SLOTS.LEFT);
47 | battleField.playCard(PLAYERS.ONE, fire, SLOTS.RIGHT);
48 | battleField.playCard(PLAYERS.TWO, fire, SLOTS.RIGHT);
49 | let result = battleField.resolve();
50 | expect(result).toBe(false);
51 | expect(battleField.isOver()).toBe(result);
52 | expect(battleField.players[PLAYERS.ONE].discardPile.length).toBe(0);
53 | expect(battleField.players[PLAYERS.TWO].discardPile.length).toBe(0);
54 |
55 | battleField.playCard(PLAYERS.ONE, fire, SLOTS.LEFT);
56 | battleField.playCard(PLAYERS.TWO, water, SLOTS.CENTER);
57 | battleField.playCard(PLAYERS.ONE, fire, SLOTS.CENTER);
58 | battleField.playCard(PLAYERS.TWO, water, SLOTS.LEFT);
59 | battleField.playCard(PLAYERS.ONE, fire, SLOTS.RIGHT);
60 | battleField.playCard(PLAYERS.TWO, water, SLOTS.RIGHT);
61 | result = battleField.resolve();
62 | expect(result).toBe(true);
63 | expect(battleField.isOver()).toBe(result);
64 | expect(battleField.result().winner).toBe(PLAYERS.TWO);
65 | expect(battleField.players[PLAYERS.ONE].discardPile.length).toBe(3); // lost three slots
66 | expect(battleField.players[PLAYERS.TWO].discardPile.length).toBe(0); // won three slots
67 | expect(battleField.players[PLAYERS.TWO].hand.length).toBe(3); // cards return to hand
68 | expect(battleField.status()[PLAYERS.ONE].life).toBe(-40);
69 | });
70 |
71 |
72 | test('set hands will get the right number of cards', () => {
73 | const deckOne = new Deck(cardGenerator.generate());
74 | const playerOne = { life: 20, deck: deckOne };
75 | const deckTwo = new Deck(cardGenerator.generate());
76 | const playerTwo = { life: 20, deck: deckTwo };
77 | const battleField = new BattleField(playerOne, playerTwo);
78 | battleField.setHand(PLAYERS.ONE);
79 | battleField.setHand(PLAYERS.TWO);
80 |
81 | expect(battleField.players[PLAYERS.ONE].hand.length).toBe(4);
82 | expect(battleField.players[PLAYERS.TWO].hand.length).toBe(4);
83 | });
84 |
85 | test('it can be serialize/deserialized to/from a plain json object', () => {
86 | const cards = cardGenerator.generate(CARDS_IN_DECK);
87 | const deck = new Deck(cards);
88 | const playerOne = { name: 'uno', deck };
89 | const playerTwo = { name: 'due', deck };
90 | const cost = 10;
91 | const battleField = new BattleField(playerOne, playerTwo);
92 | const fire = new Card({ name: '', type: TYPES.FIRE, cost });
93 | const water = new Card({ name: '', type: TYPES.WATER, cost });
94 | expect(battleField.players[PLAYERS.ONE].discardPile.length).toBe(0);
95 | expect(battleField.players[PLAYERS.TWO].discardPile.length).toBe(0);
96 | battleField.forceTurn(PLAYERS.ONE);
97 | battleField.playCard(PLAYERS.ONE, fire, SLOTS.LEFT);
98 | battleField.playCard(PLAYERS.TWO, fire, SLOTS.CENTER);
99 | battleField.playCard(PLAYERS.ONE, fire, SLOTS.CENTER);
100 | battleField.playCard(PLAYERS.TWO, fire, SLOTS.LEFT);
101 | battleField.playCard(PLAYERS.ONE, fire, SLOTS.RIGHT);
102 | battleField.playCard(PLAYERS.TWO, fire, SLOTS.RIGHT);
103 | let result = battleField.resolve();
104 | expect(result).toBe(false);
105 | expect(battleField.isOver()).toBe(result);
106 | expect(battleField.players[PLAYERS.ONE].discardPile.length).toBe(0);
107 | expect(battleField.players[PLAYERS.TWO].discardPile.length).toBe(0);
108 |
109 | const plainJs = battleField.toJs();
110 | const newBattlefield = BattleField.fromJs(plainJs);
111 | newBattlefield.playCard(PLAYERS.ONE, fire, SLOTS.LEFT);
112 | newBattlefield.playCard(PLAYERS.TWO, water, SLOTS.CENTER);
113 | newBattlefield.playCard(PLAYERS.ONE, fire, SLOTS.CENTER);
114 | newBattlefield.playCard(PLAYERS.TWO, water, SLOTS.LEFT);
115 | newBattlefield.playCard(PLAYERS.ONE, fire, SLOTS.RIGHT);
116 | newBattlefield.playCard(PLAYERS.TWO, water, SLOTS.RIGHT);
117 | result = newBattlefield.resolve();
118 | expect(result).toBe(true);
119 | expect(newBattlefield.isOver()).toBe(result);
120 | expect(newBattlefield.result().winner).toBe(PLAYERS.TWO);
121 | expect(newBattlefield.players[PLAYERS.ONE].discardPile.length).toBe(3); // lost three slots
122 | expect(newBattlefield.players[PLAYERS.TWO].discardPile.length).toBe(0); // won three slots
123 | expect(newBattlefield.players[PLAYERS.TWO].hand.length).toBe(3); // cards return to hand
124 | expect(newBattlefield.status()[PLAYERS.ONE].life).toBe(-40);
125 | });
--------------------------------------------------------------------------------
/src/libs/models/battlefield.js:
--------------------------------------------------------------------------------
1 | import {randomizer} from 'uvk';
2 | import {RESOLVE_MATRIX} from "./types";
3 | import {Player} from "./player";
4 |
5 | export const SLOTS = {
6 | LEFT: 'left',
7 | CENTER: 'center',
8 | RIGHT: 'right',
9 | };
10 |
11 | export const PLAYERS = {
12 | ONE: 'player1',
13 | TWO: 'player2',
14 | };
15 |
16 |
17 | class BattleField {
18 | turn = null;
19 | oldMoved = [];
20 | moves = [];
21 | players = {
22 | [PLAYERS.ONE]: null,
23 | [PLAYERS.TWO]: null
24 | };
25 | cumulativeCosts = {
26 | [PLAYERS.ONE]: {
27 | [SLOTS.LEFT]: 0,
28 | [SLOTS.CENTER]: 0,
29 | [SLOTS.RIGHT]: 0,
30 | },
31 | [PLAYERS.TWO]: {
32 | [SLOTS.LEFT]: 0,
33 | [SLOTS.CENTER]: 0,
34 | [SLOTS.RIGHT]: 0,
35 | }
36 | };
37 |
38 | constructor(playerOne = {}, playerTwo = {}) {
39 | this.players[PLAYERS.ONE] = new Player(PLAYERS.ONE, {...playerOne});
40 | this.players[PLAYERS.TWO] = new Player(PLAYERS.TWO, {...playerTwo});
41 | }
42 |
43 | getPlayerHand(player) {
44 | return this.players[player].hand;
45 | }
46 |
47 | setHands() {
48 | Object.keys(this.players).forEach(p => this.setHand(p));
49 | }
50 |
51 | setHand(player) {
52 | this.players[player].setHand();
53 | }
54 |
55 | forceTurn(player) {
56 | this.turn = player;
57 | }
58 |
59 | toggleTurn() {
60 | this.endTurn(this.turn)
61 | }
62 |
63 | endTurn(player) {
64 | this.turn = player === PLAYERS.ONE ? PLAYERS.TWO : PLAYERS.ONE;
65 | }
66 |
67 | getTurn() {
68 | if (this.turn) {
69 | return this.turn;
70 | }
71 |
72 | if (!this.oldMoved.length && !this.moves.length) {
73 | return randomizer.pickOne(Object.values(PLAYERS));
74 | }
75 |
76 | return this.getLastMove().player === PLAYERS.ONE ? PLAYERS.TWO : PLAYERS.ONE;
77 | }
78 |
79 | isFull() {
80 | let emptySlots = 0;
81 | Object.values(PLAYERS).forEach(p => {
82 | Object.values(SLOTS).forEach(s => {
83 | if (!this.players[p].slots[s]) {
84 | emptySlots += 1;
85 | }
86 | })
87 | });
88 | return emptySlots === 0;
89 | }
90 |
91 | status() {
92 | return {
93 | [PLAYERS.ONE]: {
94 | ...this.players[PLAYERS.ONE].status()
95 | },
96 | [PLAYERS.TWO]: {
97 | ...this.players[PLAYERS.TWO].status()
98 | }
99 | }
100 | }
101 |
102 | getLastMove() {
103 | if (!this.moves.length && !this.oldMoved.length) return null;
104 | return this.moves[this.moves.length - 1] || this.oldMoved[this.oldMoved.length - 1];
105 | }
106 |
107 | isMoveValid(player, card, slot) {
108 | return !(
109 | (this.moves.length === 1 && this.moves[0].slot === slot)
110 | || (this.getTurn() !== player)
111 | || (this.players[player].slots[slot])
112 | );
113 | }
114 |
115 | playCard(player, card, slot) {
116 | if (!this.isMoveValid(player, card, slot)) {
117 | return false;
118 | }
119 |
120 | this.players[player].play(card, slot);
121 | this.moves.push({player, slot});
122 | this.endTurn(player);
123 | return true;
124 | }
125 |
126 | resolve() {
127 | Object.values(SLOTS).forEach(s => {
128 | const playerOneCard = this.players[PLAYERS.ONE].slots[s];
129 | const playerTwoCard = this.players[PLAYERS.TWO].slots[s];
130 | const result = RESOLVE_MATRIX[playerOneCard.type][playerTwoCard.type];
131 | if (result === 0) {
132 | this.setCumulativeCost(s, playerOneCard.cost, playerTwoCard.cost);
133 | } else {
134 | const loser = (result === 1) ? PLAYERS.TWO : PLAYERS.ONE;
135 | const winner = (result === 1) ? PLAYERS.ONE : PLAYERS.TWO;
136 | const loserCard = loser === PLAYERS.ONE ? playerOneCard : playerTwoCard;
137 | const winnerCard = winner === PLAYERS.ONE ? playerOneCard : playerTwoCard;
138 | this.players[loser].life -= (loserCard.cost + this.cumulativeCosts[loser][s]);
139 |
140 | this.players[loser].toDiscard(loserCard); // losing card goes to discard pile
141 | this.players[winner].toHand(winnerCard); // winning card goes back to hand
142 | this.resetCumulativeCost(s);
143 | }
144 | });
145 |
146 | this.reset();
147 | return this.isOver();
148 | }
149 |
150 | isOver() {
151 | return (this.players[PLAYERS.ONE].life <= 0 || this.players[PLAYERS.TWO].life <= 0);
152 | }
153 |
154 | result() {
155 | const winner = this.players[PLAYERS.ONE].life > this.players[PLAYERS.TWO].life
156 | ? PLAYERS.ONE : PLAYERS.TWO;
157 | const loser = winner === PLAYERS.ONE ? PLAYERS.TWO : PLAYERS.ONE;
158 | return {
159 | winner,
160 | loser,
161 | [PLAYERS.ONE]: this.players[PLAYERS.ONE].status(),
162 | [PLAYERS.TWO]: this.players[PLAYERS.TWO].status()
163 | }
164 | }
165 |
166 | reset() {
167 | Object.values(SLOTS).forEach(s => {
168 | this.players[PLAYERS.ONE].slots[s] = null;
169 | this.players[PLAYERS.TWO].slots[s] = null;
170 | });
171 | this.oldMoved = [...this.moves];
172 | this.moves = [];
173 | }
174 |
175 | resetCumulativeCost(slot) {
176 | Object.values(PLAYERS).forEach(p => this.cumulativeCosts[p][slot] = 0);
177 | }
178 |
179 | setCumulativeCost(slot, costPlayerOne, costPlayerTwo) {
180 | this.cumulativeCosts[PLAYERS.ONE][slot] += costPlayerOne;
181 | this.cumulativeCosts[PLAYERS.TWO][slot] += costPlayerTwo;
182 | }
183 |
184 | toJs() {
185 | return {
186 | players: {
187 | [PLAYERS.ONE]: this.players[PLAYERS.ONE].toJs(),
188 | [PLAYERS.TWO]: this.players[PLAYERS.TWO].toJs()
189 | },
190 | cumulativeCosts: this.cumulativeCosts,
191 | turn: this.turn,
192 | oldMoved: this.oldMoved,
193 | moves: this.moves
194 | };
195 | }
196 |
197 | static fromJs(jsObject) {
198 | const battlefield = new BattleField();
199 | battlefield.players[PLAYERS.ONE] = Player.fromJs(jsObject.players[PLAYERS.ONE]);
200 | battlefield.players[PLAYERS.TWO] = Player.fromJs(jsObject.players[PLAYERS.TWO]);
201 | battlefield.cumulativeCosts = jsObject.cumulativeCosts;
202 | battlefield.turn = jsObject.turn;
203 | battlefield.oldMoved = jsObject.oldMoved;
204 | battlefield.moves = jsObject.moves;
205 | return battlefield;
206 | }
207 | }
208 |
209 | export {BattleField}
--------------------------------------------------------------------------------