面向对象的方式,写一个井字棋小游戏

111 阅读3分钟

我正在参加「掘金·启航计划」

现在开发都是框架,偶尔也要“复古”一下,就当复习一下基础了╰( ̄▽ ̄)╭

首先是页面布局

<div class="container">
    <div class="players">
      <div id="p1"><span class="caret" hidden></span></div>
      <div class="vs"></div>
      <div id="p2"><span class="caret" hidden></span></div>
    </div>
    <div class="playground">
      <div class="row">
        <div class="square" data-index="0"></div>
        <div class="square" data-index="1"></div>
        <div class="square" data-index="2"></div>
      </div>
      <div class="row">
        <div class="square" data-index="3"></div>
        <div class="square" data-index="4"></div>
        <div class="square" data-index="5"></div>
      </div>
      <div class="row">
        <div class="square" data-index="6"></div>
        <div class="square" data-index="7"></div>
        <div class="square" data-index="8"></div>
      </div>
      <div class="overlay">
        <button class="btn start">Start</button>
        <div class="winner" hidden>
          <img class="badge" src="images/win.svg" alt="Winner">
          <div class="logo"></div>
        </div>
        <div class="draw">
        </div>
      </div>
    </div>
    <div class="controls">
      <button class="btn reset">Reset</button>
    </div>
  </div>

然后编写一下样式

html {
  font-size: 10px;
}

body {
  margin: 0;
  background: #f8f8f8;
}

a {
  color: #0d77cc;
  text-decoration: none;
}

[hidden] {
  display: none !important;
}

.container {
  width: 27rem;
  margin: auto;
  padding: 2rem 0;
}

.players {
  width: 27rem;
  margin: auto;
  position: relative;
}

.row {
  border-bottom: 1px solid #ccc;
}

.players, .row {
  display: flex;
}

.players > div {
  flex: 1;
  height: 9rem;
  background-size: 80%;
  background-position: 50%;
  background-repeat: no-repeat;
}

.players .vs {
  background-size: 60%;
  background-image: url('../images/vs.png');
}

.players #p1, .players, #p2 {
  position: relative;
}

.players .caret {
  top: -3px;
  left: 3.5rem;
  position: absolute;
  opacity: .85;
  border-top: 1rem solid #555;
  border-left: 1rem solid transparent;
  border-right: 1rem solid transparent;
}

.players .vue, .square.vue {
  background-size: 78%;
  background-image: url('../images/vue.png');
}

.players .react {
  background-size: 83%;
  background-image: url('../images/react.png');
}

.playground {
  margin: 10px 0;
  border-radius: 2px;
  background: #fff;
  position: relative;
  border: 1px solid #ddd;
  box-shadow: 0 0 2px #ccc;
  -webkit-tap-highlight-color: rgba(0,0,0,0);
}

.playground .overlay {
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
  position: absolute;
  display: flex;
  transform: scale(1);
  transition: all .5s ease;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, .9);
}

.playground .overlay.minimize {
  transform: scale(0);
}

.playground .overlay .draw {
  display: none;
  width: 100%;
  height: 100%;
  background: url(../images/draw.jpg) no-repeat;
  background-size: cover;
  z-index: 2;
  opacity: .1;
}

.overlay .draw.clean {
  opacity: .7;
}


.square {
  flex: 1;
  height: 9rem;
  cursor: pointer;
  background-size: 80%;
  background-position: 50%;
  background-repeat: no-repeat;
  border-right: 1px solid #ccc;
}

.square:last-child {
  border-right: 0;
}

.square.vue {
  background-size: 70%;
}

.square.react {
  background-image: url('../images/react.png');
  background-size: 72%;
}

.controls {
  margin-top: 3rem;
  margin-bottom: 2rem;
  text-align: center;
}

.winner {
  z-index: 10;
  top: -1rem;
  position: relative;
}

.winner .badge {
  width: 10rem;
  margin: auto;
  display: block;
}

.winner .logo {
  width: 15rem;
  height: 15rem;
  top: -1rem;
  position: relative;
  background-size: contain;
  background-repeat: no-repeat;
}

.winner.vue .logo {
  background-image: url('../images/vue.png');
}

.winner.react .logo {
  background-image: url('../images/react.png');
}

.btn {
  width: 100px;
  height: 36px;
  border: 0;
  outline: 0;
  color: #fff; 
  cursor: pointer;
  font-size: 16px;
  font-weight: bold;
  border-radius: 4px;
  letter-spacing: 1px;
  -webkit-appearance: none;
  box-shadow: 0 0 1px #ccc;
}

.btn[disabled] {
  background: #ccc !important;
  cursor: not-allowed !important;
}

.btn.start {
  z-index: 10;
  background: #23d87c;
}

.btn.reset {
  background: #ff9800;
}

看一下最终的样子

image.png

只是个简单的小游戏,HTML和CSS部分比较简单,没什么好说的,咱们重点来理一下JS部分

首先是开始和重置两个按钮,逻辑比较简单,咱们先来实现

function Game(el) {
    this.$el = el;
    this.state = 'init';

    this.p1 = new Player(document.querySelector('#p1'));
    this.p2 = new Player(document.querySelector('#p2'));

    this.$winner = this.$el.querySelector('.winner');
    this.$overlay = this.$el.querySelector('.overlay');
    this.$playground = this.$el.querySelector('.playground');
    this.$draw = this.$el.querySelector('.draw');

    this.$start = this.$el.querySelector('.btn.start');
    this.$reset = this.$el.querySelector('.btn.reset');

    this.$reset.disabled = true;

    this.$start.addEventListener('click', this.onClickStart.bind(this));
    this.$reset.addEventListener('click', this.onClickReset.bind(this));

    this.$playground.addEventListener('click', this.onClickSquare.bind(this));
    var $squares = [].slice.call(this.$el.querySelectorAll('.square'));
    this.squares = $squares.map(function (element) {
        return new Square(element);
    });
 }
Game.prototype.start = function () {
    this.p1.setActive(true);
    this.p2.setActive(false);
    this.$overlay.hidden = true;
    this.$start.hidden = true;
    this.$reset.disabled = false;
    this.$draw.style.display = 'none';
    this.state = 'start';
};

Game.prototype.reset = function () {
    this.resetSquares();
    this.p1.setActive(false);
    this.p2.setActive(false);
    this.$winner.hidden = true;
    this.$winner.className = 'winner';
    this.$overlay.hidden = false;
    this.$start.hidden = false;
    this.$reset.disabled = true;
    this.$draw.style.display = 'none';
    this.$draw.classList.remove('clean');
    this.state = 'init';
};

Game.prototype.onClickStart = function (e) {
    this.start();
};

Game.prototype.onClickReset = function (e) {
    this.reset();
};

开始之后,自然要实现双方棋手交替下棋,并在顶部标记当前的下棋方,如下图

image.png

同时要保证,点击棋盘时,显示的是当前的棋手的棋子

function Player(el) {
    this.active = false;
    this.$el = el;
    this.name = this.$el.className;
    this.$caret = this.$el.querySelector('.caret');
}

Player.prototype.setActive = function (active) {
    this.active = !!active;
    this.$caret.hidden = !active;
};

Game.prototype.onClickSquare = function (e) {
    if (!e.target.matches('.square')) return;
    if (this.state !== 'start') return;
    if (e.target.classList.length > 1) return;
    this.squares[e.target.dataset.index].set(
        this.activePlayer().name,
        this.p1.active ? 1 : -1
    );
    var winner = this.getWinner();
    if (winner) {
    this.showWinner(winner);
        return;
    }
    this.isDraw();
    this.switchPlayer();
};

Game.prototype.activePlayer = function () {
    return this.p1.active ? this.p1 : this.p2;
};

Game.prototype.switchPlayer = function () {
    this.p1.setActive(!this.p1.active);
    this.p2.setActive(!this.p2.active);
};

效果如下图所示

image.png

解决完棋手轮流点击及棋盘显示棋子的问题,就是要判断输赢了

我们将9宫格的棋盘看成以下的样子,每一格都有一个对应的数字

image.png

然后我们就可以罗列出,所有的胜利结果的数组,同时进行输赢的判断

Game.prototype.calcWinValues = function () {
    var wins = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6],
    ];
    var result = [];
    for (var i = 0; i < wins.length; i++) {
        var val =
            this.squares[wins[i][0]].val +
            this.squares[wins[i][1]].val +
            this.squares[wins[i][2]].val;
        result.push(val);
    }
    return result;
};

Game.prototype.getWinner = function () {
    var values = this.calcWinValues();
    if (
        values.find(function (v) {
            return v === 3;
        })
    )
    return this.p1;
    if (
        values.find(function (v) {
            return v === -3;
        })
    )
    return this.p2;
};

效果如下图所示

image.png

当然还要考虑到平手的情况

Game.prototype.isDraw = function () {
    if (
        !this.squares.find(function (s) {
            return s.val === 0;
        })
    ) {
        this.$overlay.hidden = false;
        this.$draw.style.display = 'block';
        var self = this;
        setTimeout(function () {
            self.$draw.classList.add('clean');
        }, 400);
    }
};

效果如下图所示

image.png

最后,既然是面向对象的方式,自然要new一个实例啦

document.addEventListener('DOMContentLoaded', function () {
    window.game = new Game(document.querySelector('.container'));
});