最近才知道,原来WPS中的宏编程是可以用JavaScript编程的。
那么因为excel文件本身就是一个网格。我们根据一些逻辑,自然就可以实现一个俄罗斯方块的游戏啦。
模块结构
在es6中JavaScript已经支持了类的写法。那么我自然就想使用面向对象的方式去实现。
以下是关于类的分工:
- 颜色类:用于确定每个单元格的颜色。
- 单元格类:负责将x,y坐标转换为excel的坐标。以及单元个的移动和填充背景色的方法。
- 方块类:负责方块的移动和转动,其中包含了一个由单元格对象组成的列表。
- 备忘录类:用于保存页面中方格和空白的位置,以及游戏运行的判断逻辑。
- 世界类:游戏的属性类。用于维护和更新游戏中的内容。
除了这些还需要时间更新的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宏编程的俄罗斯方块游戏。