├── 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 }) => {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 | [![Build Status](https://travis-ci.org/vikkio88/elime.svg?branch=master)](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 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 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 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 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 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 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 | 5 | 6 | 7 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 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} --------------------------------------------------------------------------------