JavaScript学习笔记——编写战舰游戏 Part2:JavaScript部分

编程思路

  • 核心:跟随游戏的步骤逻辑,思考其中所需的功能,并尽量将各部分功能分离,可以使编程思路更清晰、代码易读性更强,也方便调试
  • 先写大框架,有需要的功能直接调用(虽然未编写),交给后面的代码实现细节功能
  1. 初始化:随机生成战舰的位置(这一功能最后再实现,先使用硬编码指定战舰位置(即:指定固定的位置),优先编写和调试后面的核心功能
  2. 获取玩家输入:用controller对象实现。将输入的"A3"转换为"03",判定是否击中交给后面的对象model处理;且通过model中击沉的战舰数量判断游戏是否结束;
  3. 处理击中和未击中:用model对象实现,接收到"03"这样的参数后,判断是否击中、是否重复击中同一位置、战舰是被击沉;击中/未击中的图像显示交给后面的对象view实现;
  4. 图形化的游戏显示:用view对象实现,传入击中/未击中的单元格坐标,在对应位置显示战舰或MISS图像。

摘要:知识要点

  • 每个战舰为ship对象,包含两个数组locationshits,保存一艘战舰的位置和被击中部位

  • 若有多个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对象,包含两个数组locationshitis,保存一艘战舰的位置和被击中部位
  • 若有多个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对象,包含两个数组locationshits,保存一艘战舰的位置和被击中部位
  • 若有多个ship对象,将它们保存在同一个数组ships中(数组元素为对象

思路:

  • 大循环:要生成几艘战舰,就环循几次
  • 大循环内部:
    每次在游戏板上随机生成一艘战舰(可能于已有战舰重叠)
    生成后判断是否重叠:若重叠则重新随机生成;若不重叠将新战舰加入ships数组

注意,这里的[生成-判断-若重叠,重新生成],此逻辑适合用do while循环

实现方案:

  • generateShips:主方法,它创建model对象中的ships数组且保证无重叠(战舰数由model对象的属性numShips指定)
  • generateAShipRandomly:这个方法随机在游戏板上生成一个战舰对象ship(包含两个数组locationshits)[生成战舰可能与已有的重叠]
    的位置可能与其他战舰重叠,也可能不重叠。
  • 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;
	}
	
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值