JSFiddle - React, Tailwind, and code Playground

by tlgreg

HTML

<link rel="stylesheet" href="https://cdn.foundation5.zurb.com/foundation.css">
<div id="app">
    <section class="row">
        <div class="small-6 columns">
            <h1 class="text-center">YOU</h1>
            <div class="healthbar">
                <div class="healthbar text-center" style="margin: 0; color: white;" :style="{ width: `${player.health}%`, backgroundColor: healthColor(player.health) }">
                  <div>{{ player.health }}</div>
                </div>
            </div>
        </div>
        <div class="small-6 columns">
            <h1 class="text-center">MONSTER</h1>
            <div class="healthbar">
                <div class="healthbar text-center" style="margin: 0; color: white;" :style="{ width: `${monster.health}%`, backgroundColor: healthColor(monster.health) }">
                  <div>{{ monster.health }}</div>
                </div>
            </div>
        </div>
    </section>
    <section class="row controls" v-if="!ingame">
        <div class="small-12 columns">
            <button id="start-game" @click="startGame">START NEW GAME</button>
        </div>
    </section>
    <section class="row controls" v-if="ingame">
        <div class="small-12 columns">
            <button id="attack" @click="playerRound('attack')">ATTACK</button>
            <button id="special-attack" @click="playerRound('special')">SPECIAL ATTACK</button>
            <button id="heal" @click="playerRound('heal')">HEAL</button>
            <button id="give-up" @click="playerRound('giveup')">GIVE UP</button>
        </div>
    </section>
    <section class="row log" v-if="actions.length > 0">
        <div class="small-12 columns">
            <ul>
                <li v-for="log in actions" :class="{ 'player-turn': log.player, 'monster-turn': !log.player }">
                  <span class="action">{{ actionIcon(log.action, log.success, log.player) }}</span> {{ log.player ? 'YOU' : 'The MONSTER' }} {{ actionText(log.action, log.success, log.ammount) }}
...

SCSS

.text-center {
    text-align: center;
}

.healthbar {
    width: 80%;
    min-width: 1em;
    height: 40px;
    background-color: #eee;
    margin: auto;
    transition: width 500ms;
    &.text-center > div {
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
    }
}

.controls, .log {
    margin-top: 30px;
    text-align: center;
    padding: 10px;
    border: 1px solid #ccc;
    box-shadow: 0px 3px 6px #ccc;
}

.turn {
    margin-top: 20px;
    margin-bottom: 20px;
    font-weight: bold;
    font-size: 22px;
}

.log ul {
    list-style: none;
    font-weight: bold;
    text-transform: uppercase;
}

.log ul li {
    margin: 5px;
}

.log ul .player-turn {
    color: blue;
    background-color: #e4e8ff;
}

.log ul .monster-turn {
    color: red;
    background-color: #ffc0c1;
}

button {
    font-size: 20px;
    background-color: #eee;
    padding: 12px;
    box-shadow: 0 1px 1px black;
    margin: 10px;
}

#start-game {
    background-color: #aaffb0;
}

#start-game:hover {
    background-color: #76ff7e;
}

#attack {
    background-color: #ff7367;
}

#attack:hover {
    background-color: #ff3f43;
}

#special-attack {
    background-color: #ffaf4f;
}

#special-attack:hover {
    background-color: #ff9a2b;
}

#heal {
    background-color: #aaffb0;
}

#heal:hover {
    background-color: #76ff7e;
}

#give-up {
    background-color: #ffffff;
}

#give-up:hover {
    background-color: #c7c7c7;
}

button {
  user-select: none;
}

Babel + JSX

// easy monster, less random wins if alternating between "attack" and "special attack"
// spamming only "attack" or only "special attack" mostly means a loss
// with alternating healing is rarely needed

const between = (from, to, num) => num < from ? from : num > to ? to : num
const randBetween = (min, max) => min + Math.ceil(Math.random() * (max - min))
const PLAYER_DEFAULTS = {
  health: 100,
  skill: { attack: 0.9, special: 0.25, heal: 1 },
  rage: 0,
}
const MONSTER_DEFAULTS = {
  health: 100,
  skill: { attack: 0.9, special: 0.35, heal: 1 },
  uberHeal: !(Math.random().toFixed(1)*10 % 2), // random true/false
  rage: 0.2,
}
const ACTIONS = {
  ATTACK: { MIN: 5, MAX: 12, POW: 0.7, CHANCE: 1 },
  SPECIAL: { MIN: 20, MAX: 30, POW: 0.2, CHANCE: 0.2 },
  HEAL: { MIN: 5, MAX: 15, POW: 0.8, CHANCE: 0.1 },
}

new Vue({
  el: '#app',
  data: {
    ingame: false,
    player: PLAYER_DEFAULTS,
    monster: MONSTER_DEFAULTS,
    actions: [],
  },
  methods: {
    startGame() {
      this.resetGame()
      this.ingame = true
    },
    resetGame() {
      this.actions = []
      this.player = Object.assign({}, PLAYER_DEFAULTS)
      this.monster = Object.assign({}, MONSTER_DEFAULTS)
    },
    endGame() {
      this.ingame = false
    },
    
    // gameplay
    
    playerRound(action) {
      const [success, ammount] = this.tryAction(action, this.player)
      this.actions.push({ player: true, action, success, ammount })
      if (this.monster.health <= 0) {
        this.actions.push({ player: true, success: true, action: 'win' })
        this.endGame()
      }
      if (this.ingame) this.monsterRound(action, success, ammount)
    },
    tryAction(action, character) {
      const skill = action === 'giveup'
        ? 1
        : character.skill[action] + ACTIONS[action.toUpperCase()].POW
      if (Math.random() <= skill + (action === 'special' ? character.rage : 0)) return [true, this[action](character)]
      return [false, null]
    },
    monsterRound(playerAction,...