个性化阅读
专注于IT技术分析

介绍Battlescripts:机器人,飞船,混乱!

本文概述

编程不必只涉及构建应用程序, 满足目标和满足项目规范。这也可能与获得乐趣, 享受创造事物的过程有关。许多人确实将这种技能的编程和开发视为一种娱乐形式。在srcmini, 我们想在社区中尝试一些有趣的事情。我们决定围绕Battleship建立一个bot vs bot游戏平台, 该平台现已开源。

介绍Battlescripts:机器人,飞船,混乱!

自内部首次发布以来, 该平台已吸引了我们社区中一些出色的机器人制造商的关注。社区成员之一srcmini工程师QuânLê甚至构建了一种轻松调试Battlescripts Bots的工具, 我们的确给我们留下了深刻的印象。该公告还激发了少数人创建自己的bot vs bot引擎的兴趣, 这些引擎支持不同的游戏类型和不同的规则。从《 Battlescripts》发布之日起, 就产生了惊人的想法。今天, 我们很高兴将Battlescripts开源。这为我们的社区和其他所有人提供了探索代码, 做出贡献和/或分叉代码以完全编写代码的机会。

Battlescripts剖析

Battlescript是使用一些非常简单的组件构建的。它运行在Node.js上, 并使用一些最流行且实现良好的软件包, 例如Express, Mongoose等。后端使用纯JavaScript以及前端脚本编写。该应用程序仅有的两个外部依赖项是MongoDB和Redis。用户提交的机器人代码使用Node.js随附的” vm”模块运行。在生产中, 使用Docker来提高安全性, 但并不是Battlescripts的硬性依赖。

战斗脚本

Battlescripts的代码可在GitHub上获得BSD 3条款许可。随附的README.md文件包含有关如何克隆存储库和在本地启动应用程序的详细说明。

网络服务器

你会注意到该应用程序的结构类似于简单的Express.js Web应用程序的结构。 app.js文件通过建立与数据库的连接, 注册一些常见的中间件以及定义一些社交身份验证策略来引导服务器。此外, 所有模型和路由都在” lib /”目录中定义。整个应用程序仅需要一些模型:战斗, 机器人, 挑战, 竞赛, 派对和用户。僵尸之间的争斗是在Web服务器节点外部模拟的, 并使用Node.js程序包Kue完成。这使我们能够将引擎与Web应用程序的其余部分隔离开来, 从而使战斗模拟引擎不太可能干扰Web服务器, 从而使Web应用程序本身具有更高的响应能力和稳定性。

机器人与引擎

由于这些机器人有望以JavaScript实现, 而这正是我们后端使用Node.js的能力, 因此构建引擎更加容易。在执行用户提交的代码时, 最大的挑战之一是确保代码不会在服务器上做任何恶意的事情, 或者确保任何有错误的代码都不会影响整个系统的稳定性。 Node.js的标准库附带了这个惊人的模块, 使该任务的一部分非常容易。引入” vm”模块是为了使Node.js开发人员可以更轻松地在单独的上下文中运行不受信任的代码。尽管根据官方文档, 在单独的过程中运行不受信任的代码很重要-但这是我们在生产服务器上执行的操作。在本地开发期间, ” vm”模块及其提供的功能可以正常工作。

在仍然合法的情况下, 让机器人相互抗争!

鸣叫

执行JavaScript

如果要在单独的上下文中在Node.js中运行一些任意的JavaScript代码, 则可以如下使用” vm”模块:

var vm = require(‘vm’)

var ctxObj = {
    result: ‘’
}
vm.runInNewContext(‘ result = "0xBEEF" ’, ctxObj )
console.log(ctxObj); // { result: "0xBEEF" }

在此”新上下文”中, 你运行的代码甚至无法访问” console.log”, 因为在该上下文中不存在这样的功能。但是, 你可以将原始上下文的” context.log”功能作为” ctxObj”的属性传递给新的上下文。

在Battlescripts中, 模拟战斗的节点在单独的Node.js” vm”上下文下运行每个机器人。引擎负责根据游戏规则同步两个机器人的上下文状态。

介绍Battlescripts:机器人,飞船,混乱!3

在隔离的上下文中运行JavaScript代码并不是该模块的全部功能。 ” runInNewContext”函数接受一个对象作为第三个参数, 该对象可以控制此代码执行的三个附加方面:

  • 要在与此执行相关的生成的堆栈跟踪中使用的文件名。
  • 是否将错误打印到stderr。
  • 允许执行在超时之前继续执行的毫秒数。

这个” vm”模块的陷阱之一是它不提供任何限制内存使用的方法。通过使用Docker和引擎节点的运行方式, 可以在服务器上解决此问题以及模块的其他一些限制。 ” vm”模块在经常使用时会慢慢开始泄漏难以追踪和释放的内存。即使上下文对象被重用, 内存使用量仍在增长。我们通过遵循一个简单的策略解决了这个问题。每当在工作节点中模拟战斗时, 该节点就会退出。然后, 生产服务器上的主管程序重新启动工作节点, 该工作节点准备在几分之一秒内处理下一次战斗模拟。

可扩展性

Battlescripts最初是根据战舰的标准规则设计的。里面的引擎不是很容易扩展。但是, 在发布Battlescripts之后, 最常见的要求之一就是引入更新的游戏类型, 因为该应用程序的用户很快意识到某些游戏比其他游戏更容易被机器人所征服。例如, 如果将TicTacToe与Chess进行比较, 则前者的状态空间要小得多, 这使得机器人很容易想出一种解决方案, 该解决方案将赢得或结束一场平局。

最近对Battlescripts引擎进行了一些修改, 以使其更容易引入更新的游戏类型。这可以通过简单地遵循带有少数类似钩子函数的构造来完成。由于它易于遵循, 因此在代码库中添加了另一种游戏类型TicTacToe。与该游戏类型相关的所有内容都可以在” lib / games / tictactoe.js”文件中找到。

但是, 在本文中, 我们将研究战舰游戏类型的实现。 TicTacToe游戏代码的探索可以留作以后的练习。

战舰

在研究游戏的实现方式之前, 让我们先看一下Battlescript的标准bot是什么样的:

function Bot() {}
Bot.prototype.play = function(turn) {
    // ...
}

就是这样。每个漫游器都被定义为具有” play”方法的构造函数。该方法每转一个参数就会调用一次。对于任何游戏, 该参数都是具有一种方法的对象, 该方法允许机器人在转弯时移动, 并可以附带一些表示游戏状态的其他属性。

如前所述, 最近对引擎进行了一些修改。所有战舰特定的逻辑均已从实际发动机代码中剔除。由于引擎仍在进行繁重的工作, 因此定义Battleship游戏的代码非常简单, 轻巧。

function Battleships(bot1, bot2) {
	return new Engine(bot1, bot2, {
		hooks: {
			init: function() {
				// ...
			}, play: function() {
				// ...
			}, turn: function() {
				// ...
			}
		}
	})
}

module.exports = exports = Battleships

请注意, 我们在这里仅定义了三个类似钩子的函数:init, play和turn。每个函数都以引擎为上下文来调用。从构造函数中实例化作为引擎对象的” init”函数。通常, 在这里应该准备引擎的所有状态属性。每个游戏必须准备的一个这样的属性是”网格”和(可选)”棋子”。它应该始终是一个包含两个元素的数组, 每个元素一个, 代表游戏板的状态。

for(var i = 0; i < this.bots.length; ++i) {
	var grid = []
	for(var y = 0; y < consts.gridSize.height; ++y) {
		var row = []
		for(var x = 0; x < consts.gridSize.width; ++x) {
			row.push({
				attacked: false
			})
		}
		grid.push(row)
	}
	this.grids.push(grid)
	this.pieces.push([])
}

在游戏开始之前立即调用第二个钩子” play”。这很有用, 因为这使我们有机会做一些事情, 例如代表机器人将游戏片段放置在棋盘上。

for(var botNo = 0; botNo < this.bots.length; ++botNo) {
	for(var i = 0; i < consts.pieces.length; ++i) {
		var piece = consts.pieces[i]
		for(var j = 0; j < piece.many; ++j) {
			var pieceNo = this.pieces[botNo].length

			var squares = []
			for(var y = 0; y < consts.gridSize.height; ++y) {
				for(var x = 0; x < consts.gridSize.width; ++x) {
					squares.push({
						x: x, y: y, direction: 'h'
					})
					squares.push({
						x: x, y: y, direction: 'v'
					})
				}
			}
			var square = _.sample(squares.filter(function(square) {
				var f = {
					'h': [1, 0], 'v': [0, 1]
				}
				for(var xn = square.x, yn = square.y, i = 0; i < piece.size; xn += f[square.direction][0], yn += f[square.direction][1], ++i) {
					var d = [[0, -1], [0, 1], [-1, 0], [1, 0], [-1, -1], [-1, 1], [1, -1], [1, 1]]
					for(var j = 0; j < d.length; ++j) {
						var xp = xn+d[j][0]
						var yp = yn+d[j][1]
						if(xp >= 0 && xp < 10 && yp >= 0 && yp < 10 && this.grids[botNo][yp][xp].pieceNo >= 0) {
							return false
						}
					}
					if(xn >= consts.gridSize.width || yn >= consts.gridSize.height || this.grids[botNo][yn][xn].pieceNo >= 0) {
						return false
					}
				}
				return true;
			}.bind(this)))

			switch(true) {
				case square.direction === 'h':
					for(var k = square.x; k < square.x+piece.size; ++k) {
						this.grids[botNo][square.y][k].pieceNo = pieceNo
					}
					break

				case square.direction === 'v':
					for(var k = square.y; k < square.y+piece.size; ++k) {
						this.grids[botNo][k][square.x].pieceNo = pieceNo
					}
					break

			}

			this.pieces[botNo].push({
				kind: piece.kind, size: piece.size, x: square.x, y: square.y, direction: square.direction, hits: 0, dead: false
			})
		}
	}
}

乍一看这可能有点让人不知所措, 但是这段代码实现的目标很简单。它生成碎片阵列, 每个机器人一个, 并将它们以统一的方式放置在相应的网格上。对于每一块, 都将扫描网格并将每个有效位置存储在一个临时数组中。有效位置是两块不重叠或不共享相邻单元格的位置。

最后, 第三个钩和最后一个钩”转”。与其他两个挂钩不同, 这个挂钩略有不同。这个钩子的目的是返回一个对象, 引擎将其用作调用机器人的play方法的第一个参数。

return {
	attack: _.once(function(x, y) {
		this.turn.called = true

		var botNo = this.turn.botNo
		var otherNo = (botNo+1)%2

		var baam = false

		var square = this.grids[otherNo][y][x]
		square.attacked = true
		if(square.pieceNo >= 0) {
			baam = true
			this.turn.nextNo = botNo

			var pieceNo = square.pieceNo
			var pieces = this.pieces[otherNo]
			var piece = pieces[pieceNo]
			piece.hits += 1

			if(piece.hits === piece.size) {
				piece.dead = true
				baam = {
					no: pieceNo, kind: piece.kind, size: piece.size, x: piece.x, y: piece.y, direction: piece.direction
				}
			}

			var undead = false
			for(var i = 0; i < pieces.length; ++i) {
				if(!pieces[i].dead) {
					undead = true
				}
			}
			if(!undead) {
				this.end(botNo)
			}
		}

		this.track(botNo, true, {
			x: x, y: y, baam: !!baam
		})

		return baam
	}.bind(this))
}

在这种方法中, 我们首先通知引擎该机器人已成功进行了移动。未能在任何情况下对任何游戏做出攻击动作的机器人都会自动将该游戏判为无效。接下来, 万一此举成功击中了船舶, 我们将确定船舶是否已被完全摧毁。如果是这样, 我们将返回被摧毁船只的详细信息, 否则我们将返回” true”以表示成功命中, 而没有任何其他信息。

在这些代码中, 我们遇到了一些属性和方法名, 这些属性和方法名在” this”可用。这些由Engine对象提供, 每个都有一些简单的行为特征:

  • this.turn.call:此操作在每次转弯之前均以false开头, 并且必须将其设置为true才能通知引擎机器人已在转弯时采取了行动。

  • this.turn.botNo:这将是0或1, 具体取决于本回合中哪个机器人在玩。

  • this.end(botNo):使用机器人编号调用此操作将结束游戏, 并将该机器人标记为胜利。以-1调用它以平局结束游戏。

  • this.track(botNo, isOkay, data, failReason):这是一种方便的方法, 可让你记录机器人的移动详细信息或移动失败的原因。最终, 这些记录的数据被用于可视化前端的仿真。

本质上, 这是在后端上完成的所有工作, 才能在此平台上实现游戏。

重玩游戏

战斗模拟结束后, 前端会将自己重定向到游戏重播页面。在这里可以看到仿真和结果, 并显示其他与游戏相关的数据。

介绍Battlescripts:机器人,飞船,混乱!4

后端使用” views /”中的” battle-view-battleships.jade”渲染此视图, 并在上下文中包含所有战斗细节。游戏的重播动画是通过前端JavaScript完成的。通过引擎的” trace()”方法记录的所有数据在此模板的上下文中可用。

function play() {
	$('.btn-play').hide()
	$('.btn-stop').show()

	if(i === moves.length) {
		i = 0
		stop()
		$('.ul-moves h4').fadeIn()
		return
	}
	if(i === 0) {
		$('.ul-moves h4').hide()
		$('table td').removeClass('warning danger')
		$('.count span').text(0)
	}

	$('.ul-moves li').slice(0, $('.ul-moves li').length-i).hide()
	$('.ul-moves li').slice($('.ul-moves li').length-i-1).show()

	var move = moves[i]
	var $td = $('table').eq((move.botNo+1)%2).find('tr').eq(move.data.y+1).find('td').eq(move.data.x+1)
	if(parseInt($td.text()) >= 0) {
		$td.addClass('danger')
	} else {
		$td.addClass('warning')
	}
	++i

	$('.count span').eq(move.botNo).text(parseInt($('.count span').eq(move.botNo).text())+1)

	var delay = 0
	switch(true) {
		case $('.btn-fast').hasClass('active'):
			delay = 10
			break

		case $('.btn-slow').hasClass('active'):
			delay = 100
			break

		case $('.btn-slower').hasClass('active'):
			delay = 500
			break

		case $('.btn-step').hasClass('active'):
			stop()
			return
	}
	playTimer = setTimeout(function() {
		play()
	}, delay)
}

function stop() {
	$('.btn-stop').hide()
	$('.btn-play').text(i === 0 ? 'Re-play' : ($('.btn-step').hasClass('active') ? 'Next' : 'Resume')).show()

	clearTimeout(playTimer)
}

$('.btn-play').click(function() {
	play()
})
$('.btn-stop').click(function() {
	stop()
})

接下来是什么?

现在, Battlescripts是开源的, 欢迎大家贡献。该平台目前处于成熟阶段, 但仍有很大的改进空间。它是一项新功能, 安全补丁, 甚至是错误修复, 可以在存储库中随意创建一个问题以请求解决, 或者派发存储库并提交拉取请求。如果这激发了你构建全新的内容, 请确保在下面的评论部分中告诉我们并保留指向它的链接!

赞(0)
未经允许不得转载:srcmini » 介绍Battlescripts:机器人,飞船,混乱!

评论 抢沙发

评论前必须登录!