用WPS玩俄罗斯方块

最近才知道,原来WPS中的宏编程是可以用JavaScript编程的。

那么因为excel文件本身就是一个网格。我们根据一些逻辑,自然就可以实现一个俄罗斯方块的游戏啦。

模块结构

在es6中JavaScript已经支持了类的写法。那么我自然就想使用面向对象的方式去实现。

以下是关于类的分工:

  1. 颜色类:用于确定每个单元格的颜色。
  2. 单元格类:负责将x,y坐标转换为excel的坐标。以及单元个的移动和填充背景色的方法。
  3. 方块类:负责方块的移动和转动,其中包含了一个由单元格对象组成的列表。
  4. 备忘录类:用于保存页面中方格和空白的位置,以及游戏运行的判断逻辑。
  5. 世界类:游戏的属性类。用于维护和更新游戏中的内容。

除了这些还需要时间更新的tick方法,以及鼠标和键盘的交互方法,这些都没有封装到类中。

WPS宏编程方式

在wps文件中,点击工具 => 开发工具 => WPS宏编辑器;

添加控件

在编辑器中添加一个窗口,放置几个按钮;

表单逻辑的代码:

function UserForm1_Initialize()
{
	UserForm1.StartUpPostion = 1;
	UserForm1.Left = 560; //设置窗口的出生位置
	UserForm1.Top = 120;
	changeButtonColor(0) 对窗口中的每一个按钮上颜色
}

function changeButtonColor(index){ //对窗口中的每一个按钮上颜色
	for (let i=1; i<=4; i++){
		if (i === index) {
			UserForm1.Controls("CommandButton"+i).BackColor = -65535;
			continue;
		}
		UserForm1.Controls("CommandButton"+i).BackColor = 65535;
	}
}

function UserForm1_KeyDown(keycode, shift)
{
//	Range("A2").Value2 = keycode;
	//32: 空格, 37,38,39,40:左上右下
	switch (keycode){
		case 37:{
			changeButtonColor(1)
//			Range("A3").Value2 = "左";
			world.active.update("left")
			break;
		}
		case 39:{
			changeButtonColor(3)
//			Range("A3").Value2 = "右";
			world.active.update("right")
			break;
		}
		case 40:{
			changeButtonColor(2)
//			Range("A3").Value2 = "下";
			while (world.active.update("down")){
				
			}
			break;
		}
		case 38:{
			changeButtonColor(4)
//			Range("A3").Value2 = "空";
			world.active.rotate()
			break;
		}
		case 32:{
			changeButtonColor(4)
//			Range("A3").Value2 = "空";
			world.active.rotate()
			break;
		}
	}
}

颜色类

应该把themeColor写成类方法的,但是不太会,就没有那么实现。

var themeColor = 3 //本来想写成一个类属性的,奈何不太会写,只能写外面了。用于设置方块的颜色。

class ELSColor{
	
	#update(){ //设置下一个方块的颜色。
		themeColor += 1
		if (themeColor === 11) {
			themeColor = 3
		}
		
	}
	constructor(){
		this.color = themeColor; //设置当前方块的颜色
		this.#update() //更新下一个方块的颜色。
	}
	
	say(){ //用于调试,检查颜色。
		console.log('my color is ' + this.color)
	}
}

单元格类

class ELSUnit{ //每个单元的类,用与设置单元格的背景色,方块是由若干个单元对象组合而成的。
	constructor(x, y, color) {
		this.x = x //下标从1开始
		this.y = y //下标从1开始
		this.color = color //颜色类的对象
		this.getPosition() //将x,y坐标转换为excel的坐标表示。
		
	}
	
	getPosition(){ //将x, y坐标转换为wps的坐标表示
		let A = "A".charCodeAt();
		let S = String.fromCharCode(A + this.y - 1); //计算列名
		this.position = S + this.x;
		return this.position;
	}
	
	moveRight(){
		this.y += 1
		if (this.y == 11) {
			this.y = 10
		}
		this.getPosition()
	}
	
	moveLeft(){
		this.y -= 1
		if (this.y === 0) {
			this.y = 1
		}
		this.getPosition()
	}
	
	moveDown(){
		this.x += 1
		this.getPosition()
	}
	
	Move(direction){
		if (direction === "left"){
			this.moveLeft()
		}else if (direction === "right") {
			this.moveRight()
		}else if (direction === "down"){
			this.moveDown()
		}
		this.getPosition()
		this.Paint()
	}
	
	Add_x(value){ //更新坐标位置
		this.x += value;
		this.getPosition()
	}
	
	Paint(force = false){
		if ((this.x < 1 || this.x > 13 || this.y < 1 || this.y > 10) && (! force) ) return;
		(obj=>{ // 为每个单元格上颜色
			obj.Pattern = xlPatternSolid;
			obj.ThemeColor = this.color;
			obj.TintAndShade = 0;
		})(Range(this.position).Interior);
		
		//为每个单元格上边框
		(obj=>{
		obj.Weight = xlMedium;
		obj.LineStyle = xlContinuous;
		})(Range(this.position).Borders.Item(xlEdgeLeft));
		(obj=>{
			obj.Weight = xlMedium;
			obj.LineStyle = xlContinuous;
		})(Range(this.position).Borders.Item(xlEdgeTop));
		(obj=>{
			obj.Weight = xlMedium;
			obj.LineStyle = xlContinuous;
		})(Range(this.position).Borders.Item(xlEdgeBottom));
		(obj=>{
			obj.Weight = xlMedium;
			obj.LineStyle = xlContinuous;
		})(Range(this.position).Borders.Item(xlEdgeRight));
		(obj=>{
			obj.ColorIndex = xlColorIndexAutomatic;
			obj.ColorIndex = xlColorIndexAutomatic;
			obj.TintAndShade = 0;
		})(Range(this.position).Borders.Item(xlEdgeLeft));
		(obj=>{
			obj.ColorIndex = xlColorIndexAutomatic;
			obj.ColorIndex = xlColorIndexAutomatic;
			obj.TintAndShade = 0;
		})(Range(this.position).Borders.Item(xlEdgeTop));
		(obj=>{
			obj.ColorIndex = xlColorIndexAutomatic;
			obj.ColorIndex = xlColorIndexAutomatic;
			obj.TintAndShade = 0;
		})(Range(this.position).Borders.Item(xlEdgeBottom));
		(obj=>{
			obj.ColorIndex = xlColorIndexAutomatic;
			obj.ColorIndex = xlColorIndexAutomatic;
			obj.TintAndShade = 0;
		})(Range(this.position).Borders.Item(xlEdgeRight));
		Application.DisplayAlerts = false;
		Application.DisplayAlerts = true;
	}
	
	unPaint(){ //将单元格对应位置变为空白。
		if (this.x < 1 || this.x > 13 || this.y < 1 || this.y > 10) return;
		Range(this.getPosition()).Clear();
//		Range(this.position).Clear();
	}
	
}

方块类

俄罗斯方块一共包含"I", "J", "L", "O", "S", "Z", "T"这么多种类型的方块。

我写了一个方块的基类,其他类需要继承这个基类。在这里我只展示基类和J型方块的代码。


//=====================方块基类=====================================================
class ELSBlock{
	constructor() {
		this.color = new ELSColor().color
		this.unitArr = []
		this.displayArr = []
		this.centerIndex = 1;
	}
	
	display() {
		this.displayArr.forEach(element => { 
			element.Paint(true)
		})
	}
	
	update(direction) { //left, right, down
		let canMove = world.memo.CheckCanMove(this, direction)
		if (! canMove) return false; //如果不能移动就返回
		
		this.unitArr.forEach(element => { //先全部清除
			element.unPaint();
		})
		
		this.unitArr.forEach(element => { //再全部填充
			element.Move(direction)
		})
		
		return true
	}
	
	canRotate(){
		var center = this.unitArr[this.centerIndex]
		for (let i=0; i<4; i++){
			var element = this.unitArr[i]
			let distX = element.x - center.x
			let distY = element.y - center.y
			const y = center.y + distX
			const x = center.x - distY
			if (x < 1 || x > 13 || y < 1 || y > 10) return false;
			if (world.memo.memo[x][y] === 1) {
				return false
			}
		}
		return true
		
	}
	
	rotate() {
		if (! this.canRotate()) return;
		
		this.unitArr.forEach(element => { //先全部清除
			element.unPaint();
		})
		
		var center = this.unitArr[this.centerIndex]
		this.unitArr.forEach(element => {
			let distX = element.x - center.x
			let distY = element.y - center.y
			element.y = center.y + distX
			element.x = center.x - distY
			
			element.Move("doNotMove")
		})

	}
	
}

//=====================J型方块=====================================================
class ELSBlockJ extends ELSBlock{ //Array("I", "J", "L", "O", "S", "Z", "T")
	constructor(){
		super()
		this.style = this.getStyle();
		this.build()
		this.buildDisplayArr();
		this.centerIndex = 2;
	}
	
	getStyle(){
		var styleList = ["row", "col"];
		return getRandomElement(styleList);
	}
	
	build(){
		const length = 3;
		if (this.style === "row"){
			for (let i=0; i< length; i++){
				this.unitArr.push(new ELSUnit(-1, i+4, this.color))
			}
			this.unitArr.push(new ELSUnit(0, length+3, this.color))
		}else{
			for (let i=0; i< length; i++){
				this.unitArr.push(new ELSUnit(i-2, 6, this.color))
			}
			this.unitArr.push(new ELSUnit(0, 5, this.color))
		}
		
	}
	
	buildDisplayArr(){
		const length = 3;
		if (this.style === "row"){
			for (let i=0; i< length; i++){
				this.displayArr.push(new ELSUnit(4, i+15, this.color))
			}
			this.displayArr.push(new ELSUnit(5, 17, this.color))
		}else{
			for (let i=0; i< length; i++){
				this.displayArr.push(new ELSUnit(3+i, 17, this.color))
			}
			this.displayArr.push(new ELSUnit(5, 16, this.color))
		}
	}
	

}

备忘录类

class MEMO{
	constructor(){
		 //13行10列的二维数组,用于保存容器中的方块。
		this.memo = new Array(14).fill(0).map(() => new Array(11).fill(0));//由于在unit中是从1开始数,所以0行0列忽略  
		this.units = []
	}
	
	CheckCanMove(block, direction){ //给我传过来的是ELSBlock对象。
		let canMove = true
		if (direction === "down") {
			block.unitArr.forEach(element => {
				if (element.x >= 0){ //因为刚开始生成的时候是在边界外的,所以特意加了这个条件
					if (element.x >= 13 || this.memo[element.x+1][element.y] === 1){
						canMove = false;
					}
				}
			})
			if (! canMove) { //如果不能向下走了,那么就判定为沉底了。
				this.nextProcess(block);
			}
		} else if (direction === "left") {
			block.unitArr.forEach(element => {
				if (element.x >= 0){ //因为刚开始生成的时候是在边界外的,所以特意加了这个条件
					if (element.y <= 1 || this.memo[element.x][element.y-1] === 1){
						canMove = false;
					}
				}
			})
		} else if (direction === "right") {
			block.unitArr.forEach(element => {
				if (element.x >= 0){ //因为刚开始生成的时候是在边界外的,所以特意加了这个条件
					if (element.y >= 10 || this.memo[element.x][element.y+1] === 1){
						canMove = false;
					}
				}
			})
		}
		return canMove;
	}
	
	nextProcess(block) {
		block.unitArr.forEach(element => {
			this.units.push(element) //从活动方块,添加到容器中
			var die = this.CheckGameOver(element.x); //在这里判断游戏有没有结束
			if (! die) this.memo[element.x][element.y] = 1; //如果游戏没结束,则更新memo
		})
		//TODO: 判断是否可以消去一行了
		this.CleanLine()
		world.updateBlock(); //活动方块更新
//		world.active = new ELSBlockI()
	}
	
	CleanLine(){  //更新库存中的方块
		var sholdCleanLine = this.getCleanLine()
		if (sholdCleanLine.length === 0) return; //如果没有要清除的行,就返回。
		
		this.units.forEach(element => { //将画布清空;
			element.unPaint()
		})
		
		//更新分数
		world.updateScore(10 * sholdCleanLine.length);
		
		var units_copy = []
		sholdCleanLine.sort((a, b) => a-b);
		let count = 0
		this.units.forEach(element => {
			if ( sholdCleanLine.includes(element.x) ){
				count += 1;
			}else{
				var bias = 0;
				for (let index = 0; index <= sholdCleanLine.length; index++){
					if (sholdCleanLine[index] > element.x){
						bias = sholdCleanLine.length - index
						break;
					}
				}
				element.Add_x(bias);
				units_copy.push(element)
			}
		})
		
		this.CleanLinePaint(units_copy); //更新画面
		
		this.units = units_copy; //更新库存
	}
	
	getCleanLine(){
		var sholdCleanLine = []
		for (let i=1; i <= 13; i++){
			let sholdClean = true;
			for (let j=1; j <= 10; j++){
				if (this.memo[i][j] === 0) {
					sholdClean = false
					break
				}
			}
			if (sholdClean) sholdCleanLine.push(i)
		}
		return sholdCleanLine;
	}
	
	CleanLinePaint(after){ //max_x表示最大的行,befor表示之前的units, after表示当前的units
		
		//在这里重新绘制的时候,顺便把memo更新一下
		this.memo = new Array(14).fill(0).map(() => new Array(11).fill(0));
		after.forEach(element => {
			element.Paint()
			this.memo[element.x][element.y] = 1;
		})
	}
	
	CheckGameOver(x){ //判断游戏是否结束
		if (x <= 0){
			world.WorldStop = true;
			return true
		}
		return false
	}
}

世界类



class World{
	constructor(){
		this.active = new getRandomBlock() //当前活动的方块
		this.next_active = new getRandomBlock() //下一个活动方块
		this.WorldStop = false;
		this.memo = new MEMO();
		this.score = 0;
		this.updateScore(0);
		DisplayBlock(this.next_active); //在预览框显示下一个方块
	}
	
	getSpead(){
		return 1000;
	}
	
	tick(){
		this.active.update("down") //方块自动向下移动
	}
	
	updateScore(value){
		this.score += value; //加分。
		Range("V6").Value2 = "SCORE : " + 10 * this.score;
	}
	
	updateBlock(){
		this.active = this.next_active;
		this.next_active = new getRandomBlock();
		DisplayBlock(this.next_active);
	}
}

世界时更新逻辑:tick

function clock(){
	//更改时间模块
	var d = new Date().toLocaleString()
	Range("v2").Value2 = d;
	
	//游戏trick更新
	world.tick()
//	DoEvents()
	
	if (world.WorldStop) {
		alert("GAME OVER");
		return
	}
	
	//fps设置
	setTime(world.getSpead())
}

function setTime(spead = 1000){
	
	var d = new Date()
	var d1 = d.getTime();
	d.setTime(d1+spead); //设置更新时间为1秒,即每秒更新一下页面。
//	DoEvents();
	Application.OnTime(d.toLocaleString(), "clock")
}

控件控制逻辑


/**
 * CommandButton1_Click Macro
 */
function CommandButton1_Click()
{
//	Macro3()
	world = new World();
	UserForm1.Show();
	UserForm1_Initialize();
	
	//运行主世界
	setTime();
}

/**
 * CommandButton3_Click Macro
 */
function CommandButton3_Click()
{
	world.WorldStop = true;
}

/**
 * CommandButton5_Click Macro
 */
function CommandButton5_Click()
{
	if (world.WorldStop) {
		Range("A1:J13").Clear();
	}
}

/**
 * UserForm1_CommandButton1_Click Macro
 */
function UserForm1_CommandButton1_Click()
{
		changeButtonColor(1)
		world.active.update("left")
}

/**
 * UserForm1_CommandButton3_Click Macro
 */
function UserForm1_CommandButton3_Click()
{
		changeButtonColor(3)
		world.active.update("right")
}
/**
 * UserForm1_CommandButton2_Click Macro
 */
function UserForm1_CommandButton2_Click()
{
	changeButtonColor(2)
	while (world.active.update("down")){
		
	}
}
/**
 * UserForm1_CommandButton4_Click Macro
 */
function UserForm1_CommandButton4_Click()
{
	changeButtonColor(4)
	world.active.rotate()
}

其他逻辑


function DisplayBlock(Block){
	Range("O3:R6").Clear(); //清理展示区
	Block.display();
}

function getRandomElement(arr) {
    if (!Array.isArray(arr) || arr.length === 0) {
        return undefined; // 如果输入不是数组或数组为空,返回undefined
    }
    const randomIndex = Math.floor(Math.random() * arr.length); // 生成一个随机索引
    return arr[randomIndex]; // 返回随机索引处的元素
}

function getRandomBlock(){
	var BlockList = [ELSBlockI, ELSBlockJ, ELSBlockL,ELSBlockO,ELSBlockT,ELSBlockS,ELSBlockZ];
//	var BlockList = [ELSBlockZ];
	var randomItem = getRandomElement(BlockList);
	return new randomItem();
}

总结

通篇写下来感觉代码还挺多,不知道网上几百行的都是如何实现的。

文件我上传到了GitHub上,链接跳转:

GitHub - Zhenghong-Liu/WPS-Tetris: A Tetris game program by wps.js 一个使用wps宏编程的俄罗斯方块游戏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值