编程思路
- 核心:跟随游戏的步骤逻辑,思考其中所需的功能,并尽量将各部分功能分离,可以使编程思路更清晰、代码易读性更强,也方便调试
- 先写大框架,有需要的功能直接调用(虽然未编写),交给后面的代码实现细节功能
- 初始化:随机生成战舰的位置(这一功能最后再实现,先使用硬编码指定战舰位置(即:指定固定的位置),优先编写和调试后面的核心功能)
- 获取玩家输入:用controller对象实现。将输入的"A3"转换为"03",判定是否击中交给后面的对象model处理;且通过model中击沉的战舰数量判断游戏是否结束;
- 处理击中和未击中:用model对象实现,接收到"03"这样的参数后,判断是否击中、是否重复击中同一位置、战舰是被击沉;击中/未击中的图像显示交给后面的对象view实现;
- 图形化的游戏显示:用view对象实现,传入击中/未击中的单元格坐标,在对应位置显示战舰或MISS图像。
摘要:知识要点
-
每个战舰为ship对象,包含两个数组
locations
和hits
,保存一艘战舰的位置和被击中部位 -
若有多个ship对象,将它们保存在同一个数组ships中(数组元素为对象)
-
玩家提交输入后,通过遍历每艘战舰的位置,判定是否击中
-
根据是否击中,使用对应网格的
<td>
元素的setAttribute()
将其设置为之前在CSS中规定好的类(类的background
已被设置为战舰或MISS图像),网页将实时地根据其class
(类)的不同改变其背景图像 -
消息显示区为
<div>
元素,innerHTML
对应其文本 -
输入框为
<input>
元素(type
为"text"
),value
对输入框的内容,placeholder
可预设其文本,将value
置为""
可清空文本 -
开火按钮为
<input>
元素(type
为"button"
),value
对应按钮上的文本 -
"button"
型<input>
元素的onclick
事件:检测到点击事件时触发 -
[优化:回车自动提交表单]
"text"
型<input>
元素的onkeypress
事件:(在输入框中)检测到按键事件时触发 -
[优化:回车自动提交表单]事件处理程序:判断当前按下的按键是否为回车,如果是,自动调用
fireButton.click()
实现按钮点击操作
设计方案
将这款游戏设计为几个较为独立的对象(每个对象有其主要职责,从而便于独立创建和测试游戏的各个部分),并利用DOM与用户交互。这种设计让问题理解起来容易得多。
- 这样做的优点在于:可以相对独立的编写代码,并分别测试每个对象,确保对象履行其职责(每个对象的测试代码可能需要单独编写,仅用于测试该对象的功能)
总共有3个要设计并实现的对象:model、view和controller
- model将存储游戏的状态,如每艘战舰的位置以及哪个部位被击中;
- view负责在model存储的状态改变时更新界面;
- controller将各个部分整合以实现游戏逻辑:将用户输入交给model以更新状态、判断游戏是否结束
1.view对象
- view对象用于显示当前的游戏状态
- 需要displayMessage方法:在左上角显示hit/miss/you sank my battleship信息
- 需要displayHit方法:在某指定网格内显示战舰图像
- 需要displayMiss方法:在某指定网格内显示MISS图像
具体实现方案
displayMessage方法:传入要显示的信息msg
,通过getElementById
获取用于显示消息的<div>
元素,用innerHTML
设置其文本为msg
displayHit方法:传入指定网格<td>
的id,通过getElementById
获取相应的<td>
元素,并将其class
属性设置为hit类,即可将网格背景设置为战舰图像(利用了之前定义的hit类,其background
属性为战舰图像)
displayMiss方法:同理
var view = {
displayMessage: function(msg) {
var messageArea = document.getElementById("messageArea");
messageArea.innerHTML = msg;
},
displayHit: function(location) {
var cell = document.getElementById(location);
cell.setAttribute("class", "hit");
},
displayMiss: function(location) {
var cell = document.getElementById(location);
cell.setAttribute("class", "miss");
}
};
2.model对象
model对象用于存储游戏的状态、判断是否击中以完成状态转换、状态改变时通知view
- 需要一些属性:保存网格大小、总战舰数量、战舰长度、被击沉的战舰数
- 每个战舰为ship对象,包含两个数组
locations
和hitis
,保存一艘战舰的位置和被击中部位 - 若有多个ship对象,将它们保存在同一个数组ships中(数组元素为对象)
- 需要fire方法:向战舰开火时给出开火位置,判断是否击中、击沉,并更新状态
- 需要isSunk方法:返回战舰是否被击沉
- 注意model对象所保存的状态改变时(如击中),要通知view。因为view模型并不会主动的检查状态的变化
- 需要generateShip方法:随机生成战舰
核心逻辑:如何检测击中
遍历每艘船,比较船的位置和开火的位置
var model = {
boardSize: 7,
numShips: 3,
shipLength: 3,
shipsSunk: 0,
// hard-coded values for ship locations(used for test)
/*
ships: [
{ locations: ["06", "16", "26"], hits: ["", "", ""] },
{ locations: ["24", "34", "44"], hits: ["", "", ""] },
{ locations: ["10", "11", "12"], hits: ["", "", ""] }
],
*/
fire: function(guess) {
for (var i = 0; i < this.numShips; i++) {
var ship = this.ships[i];
var index = ship.locations.indexOf(guess);//猜测的位置上有战舰
if (ship.hits[index] === "hit") {
//Already hit
return true;
} else if (index >= 0) {
//HIT
if (this.isSunk(ship)) {
//SUNK
}
return true;
}
}
//MISS
return false;
},
//...
};
注意,查看是否命中战舰时,代码为
var ship = this.ships[i];
var locations = ship.locations;
var index = locations.indexOf(guess);
这里使用串接(Chaining)将对象引用连接,避免创建临时变量,也减少了代码量
串接后的代码为
var ship = this.ships[i];
var index = ship.locations.indexOf(guess);
此处ship没有串接,以免串接链条过长(阅读理解困难)
3.controller对象
controller对象用于整合各部分:处理用户输入,记录猜测次数、让model根据当前猜测更新自己、判断游戏是否结束
- 需要guesses属性:记录猜测次数
- 需要processGuess方法:传入用户猜测,对其处理(如"A3"转换为"03"),将结果交给model,检测游戏是否结束
var controller = {
guesses: 0,
processGuess: function(guess) {
var location = parseGuess(guess);//"A3"转换为"03"的辅助函数
if (location) {//有效输入
this.guesses++;
var hit = model.fire(location);
if (hit && model.shipsSunk === model.numShips) {
view.displayMessage("You sank all my battleships, in " + this.guesses + " guesses");
}
}
}
}
怎样获取玩家输入:事件处理程序
事件处理程序能将HTML中的表单<form>
元素与游戏关联
- 点击
Fire!
按钮时(单击事件),调用事件处理程序 - 事件处理程序(回调函数)事先编写好,用于获取输入框的内容并交给controller
单击事件与事件处理程序
window.onload = init;
function init() {//网页加载完毕后,再开始接收输入并开始游戏
// place the ships on the game board
model.generateShipLocations();
// Fire! button onclick handler
var fireButton = document.getElementById("fireButton");
fireButton.onclick = handleFireButton;
}
事件处理程序用于获取输入框的内容并交给controller
function handleFireButton() {
var guessInput = document.getElementById("guessInput");
var guess = guessInput.value.toUpperCase();//获取输入框的内容
controller.processGuess(guess);//输入框的内容交给controller
guessInput.value = "";//重置输入框,玩家无需手动删除上一个输入
}
优化:希望按下回车也能触发事件
-
HTML中
<input>
元素的onkeypress
事件:(在输入框中)检测到按键事件时,就触发对应的事件处理程序,可将所需的按键处理程序赋给onkeypress
-
事件处理程序:判断当前按下的按键是否为回车,如果是,自动调用
fireButton.click()
实现按钮点击操作
这里使用了guessInput
元素的onkeypress
事件
- guessInput.onkeypress = handleKeyPress;
- 浏览器向事件处理程序
handleKeyPress
传递一个事件对象,其中包含有关用户按下了哪个键的信息
function init() {//网页加载完毕后,再开始接收输入并开始游戏
//...
var guessInput = document.getElementById("guessInput");
guessInput.onkeypress = handleKeyPress;
//...
}
function handleKeyPress(e) {//包含有关用户按下了哪个键的信息
var fireButton = document.getElementById("fireButton");
// in IE9 and earlier, the event object doesn't get passed
// to the event handler correctly, so we use window.event instead.
e = e || window.event;
if (e.keyCode === 13) {
fireButton.click();
return false;
//返回false,让表单不做其他的提交等操作
}
}
4.随机生成战舰
- 每个战舰为ship对象,包含两个数组
locations
和hits
,保存一艘战舰的位置和被击中部位- 若有多个ship对象,将它们保存在同一个数组ships中(数组元素为对象)
思路:
- 大循环:要生成几艘战舰,就环循几次
- 大循环内部:
每次在游戏板上随机生成一艘战舰(可能于已有战舰重叠)
生成后判断是否重叠:若重叠则重新随机生成;若不重叠将新战舰加入ships
数组
注意,这里的[生成-判断-若重叠,重新生成],此逻辑适合用
do while
循环
实现方案:
generateShips
:主方法,它创建model
对象中的ships数组且保证无重叠(战舰数由model对象的属性numShips
指定)generateAShipRandomly
:这个方法随机在游戏板上生成一个战舰对象ship
(包含两个数组locations
和hits
)[生成战舰可能与已有的重叠]
的位置可能与其他战舰重叠,也可能不重叠。collision
:传入ship对象,并判断它是否与已有的战舰重叠
ps. 生成新战舰时无需顾及hits
的生成:检测是否击中,看的是hits
数组中对应元素是否为"hit"
(因为被击中时才设置hits
数组中相应索引的值为"hit"
),一开始默认hits
数组为空即可
generateShips: function() {
var locations;
for (var i = 0; i < this.numShips; i++) {
do {
locations = this.generateAShipRandomly();
} while (this.collision(locations));
this.ships[i].locations = locations;
}
console.log("Ships array: ");
console.log(this.ships);
},
generateAShipRandomly: function() {
var direction = Math.floor(Math.random() * 2);
var row, col;
if (direction === 1) { // horizontal
row = Math.floor(Math.random() * this.boardSize);
col = Math.floor(Math.random() * (this.boardSize - this.shipLength + 1));
} else { // vertical
row = Math.floor(Math.random() * (this.boardSize - this.shipLength + 1));
col = Math.floor(Math.random() * this.boardSize);
}
var newShipLocations = [];
for (var i = 0; i < this.shipLength; i++) {
if (direction === 1) {
newShipLocations.push(row + "" + (col + i));
} else {
newShipLocations.push((row + i) + "" + col);
}
}
return newShipLocations;
},
collision: function(locations) {
for (var i = 0; i < this.numShips; i++) {
var ship = this.ships[i];
for (var j = 0; j < locations.length; j++) {
if (ship.locations.indexOf(locations[j]) >= 0) {
return true;
}
}
}
return false;
}
};