Code Monkey home page Code Monkey logo

human-resource-machine's Introduction

Human Resource Machine

Repository for the codes of Fundamentals of Programming, 2023 Fall

目录

构建环境

Qt Creator 12.0.1 (Enterprise)

Qt 6.6.0 以及配套版本的 MinGW 编译器

设计思路

我们选择使用 Qt Creator 进行大作业程序的编写。它提供了一条设计图形界面并将其与程序整合起来的简单途径。

在设计思路上,我们主要使用队列来模拟传送带,并使用向量来存储空地的状态。具体而言,我们在 qInqAns 两个队列中存储了当前关卡的输入序列与目标序列,并用 qOut 队列来存储当前的输出序列。在指令执行完成后,程序将比对 qAnsqOut,并以此判断关卡是否通过。

工程结构

工程结构如下表所示。

- HumanResourceMachine
	- Header Files
		- widget.h
	- Source Files
		- main.cpp
		- widget.cpp
			- void on_backButton1_clicked()
			- void on_backButton2_clicked()
			- void on_backButton3_clicked()
			- void on_startButton_clicked()
			- void on_aboutButton_clicked()
			- void on_exitButton_clicked()
			- void on_volumnHorizontalSilder_valueChanged()
			- void on_levelButton1_clicked()
			- void on_levelButton2_clicked()
			- void on_levelButton3_clicked()
			- void on_levelButton4_clicked()
			- void on_confirmNextStepButton_clicked()
			- void drawStatus()
			- void setUpBackground()
			- void printSuccessMessage()
			- void printFailMessage()
			- void printErrorMessage()
			- bool checkResult()
			- int myToInt(QString)
			- void renderLevelButton(int)
	- Resources.qrc
	- widget.ui
  • widget.h
  • main.cpp
  • widget.cpp 中实现了程序的大部分功能。
    • on_backButton1_clicked()on_backButton2_clicked()on_backButton3_clicked() 在按下不同页面的返回键时被调用。它们将程序的界面设置为上一级页面。
    • on_startButton_clicked() 在按下 开始游戏 键时被调用。它将试图读取储存于程序可执行文件所在目录中的存档 userdata.txt,并根据其解锁可游玩的关卡。随后,它将程序界面设置为关卡选择页面。
    • on_aboutButton_clicked() 在按下 设置 键时被调用。它将程序的界面设置为设置页面。
    • on_exitButton_clicked() 在按下 退出游戏 键时被调用。它将目前已经通关的关卡编号写入 userdata.txt 中,随后关闭程序。
    • on_volumnHorizontalSlider_valueChanged() 在设置界面的音量滑块移动时被调用。它将程序背景音乐的音量设置为与滑块位置相对应的音量。
    • on_levelButton1_clicked()on_levelButton2_clicked()on_levelButton3_clicked() 分别在按下第一关第二关第三关键时被调用。它们将程序的界面设置为游戏页面,并根据大作业要求中的内容设置关卡信息。
    • on_levelButton4_clicked()第四关 键被按下时被调用。它将弹出一个文件选择对话框并试图读取一个 .json 文件。若读取成功,则它将程序的界面设置为游戏页面,并根据 .json 文件中的信息设置关卡信息。
    • on_confirmNextStepButton_clicked()下一步/检查结果 键被按下时被调用。它将读入一行游戏页面文本框中玩家输入的信息并检查其合法性,随后执行相关操作。
    • drawStatus() 在游戏页面更新时被调用。它将根据当前关卡目前的游戏状态,在页面上显示相关信息(如当前输入序列、目标输出序列、空地状态)。
    • setUpBackground() 在程序界面被设置为游戏页面时被调用。它将根据当前关卡的初始信息,在页面上显示相关信息(如可用指令集、可用空地数等)。
    • printSuccessMessage() 在关卡成功通过时被调用。它弹出一个对话框,显示当前关卡已成功完成。
    • printFailMessage() 在关卡失败时被调用。它弹出一个对话框,显示关卡失败,并提供 重新开始返回主菜单 两个选项。
    • printErrorMessage() 在输入指令非法时被调用。它弹出一个对话框,显示关卡在当前位置出现了非法指令,并提供 重新开始返回主菜单 两个选项。
    • checkResult() 在输入的指令执行完成后被调用。它比对当前输出序列与目标序列,并相应调用 printSuccessMessage()printErrorMessage()
    • myToInt(QString) 在程序检查输入的指令参数时被调用。它检查当前指令的参数是否合法,若合法则返回参数,否则返回 -1
    • renderLevelButton(int) 在程序被设置为选关页面时被调用。它根据当前已通关的关卡来设置当前哪些关卡可以游玩。
  • Resources.qrc 是程序的资源文件,储存了程序的背景音乐、字体与图片等。
  • widget.ui 中存储了程序图形界面的相关设置。

具体实现

设置关卡信息

on_levelButton4_clicked() 为例。

// 开始第四关游戏
void Widget::on_levelButton4_clicked() {
    // 重置
    qIn.clear(), qOut.clear(), qAns.clear(), cmdSet.clear();
    vec.clear(), existVec.clear(), cmdLines.clear();
    existCurrentBlock = 0;
    // 设置第四关游戏配置
    currentLevel = 4;
    // 从 JSON 文件中读取关卡配置
    QString filePathName = QFileDialog::getOpenFileName(this, "打开", "./", "JSON 文件 (*.json)");
    if (filePathName.isEmpty()) {
        QMessageBox::warning(this, "警告", "已取消选择关卡!");
    } else {
        QFile openFile(filePathName);
        openFile.open(QIODevice::ReadOnly);
        QByteArray fileContents = openFile.readAll();
        openFile.close();

        QJsonObject jsonObj = QJsonDocument::fromJson(fileContents).object();

        QJsonArray jsonArray = jsonObj.value("input").toArray();

        for (const QJsonValue& element : jsonArray)
            qIn.enqueue(element.toInt());

        jsonArray = jsonObj.value("output").toArray();
        for (const QJsonValue& element : jsonArray)
            qAns.enqueue(element.toInt());

        n = jsonObj.value("vacancy").toInt();
        vec.resize(n);
        existVec.resize(n);

        jsonArray = jsonObj.value("cmd").toArray();
        for (const QJsonValue& element : jsonArray) {
            switch (element.toInt()) {
            case 0:
                cmdSet.enqueue("inbox");
                break;
            case 1:
                cmdSet.enqueue("outbox");
                break;
            case 2:
                cmdSet.enqueue("copyfrom");
                break;
            case 3:
                cmdSet.enqueue("copyto");
                break;
            case 4:
                cmdSet.enqueue("add");
                break;
            case 5:
                cmdSet.enqueue("sub");
                break;
            case 6:
                cmdSet.enqueue("jump");
                break;
            case 7:
                cmdSet.enqueue("jumpifzero");
                break;
            default:
                break;
            }
        }
        ui->stackedWidget->setCurrentIndex(3);
        setUpBackground();
    }
}

它所接受的 .json 文件应格式如下:

{
    "input": [1, 2, 7, 7, 9, 3, 3, 3],
    "output": [7, -3],
    "vacancy": 6,
    "cmd": [0, 1, 2, 3, 4, 5]
}

仓库中的 example.json 即为此例。其中 input表示初始输入序列,output 表示目标输出序列,vacancy 表示可用空地数量,cmd 表示可用指令集。

指令集中的数字对应关系如下:

指令集中数字 指令
0 inbox
1 outbox
2 copyfrom
3 copyto
4 add
5 sub
6 jump
7 jumpifzero

成功读取后,函数将 input 序列存入队列 qIn 中,将 output 序列存入队列 qAns 中,根据 vacancy 的值设置向量 vecexistVec 的大小,并根据 cmd 设置关卡的可用指令集。

最后,函数将程序界面设置为游戏页面,然后调用 setUpBackground() 函数展示关卡信息。

执行玩家输入的指令

代码如下:

void Widget::on_confirmNextStepButton_clicked() {
    // 如果还没有开始执行指令
    if (!doing) {
        // 开始读入指令
        QStringList allCmdLinesWithoutTrim = ui->cmdTextEdit->toPlainText().split("\n");

        for (const QString& element : allCmdLinesWithoutTrim) {
            QString contains;
            int len = element.length();
            for (int i = 0; i < len; i++) {
                // 连续的两个空格,则前一个省略
                if (element[i] == ' ' && i < len - 1 && element[i + 1] == ' ')
                    continue;
                // 否则直接接到 contains 里
                else
                    contains.append(element[i]);
            }
            // 再除去首尾空格
            contains = contains.trimmed();
            // 如果 contains 非空
            if (contains.size())
                cmdLines.push_back(contains);
        }
        // 指令数量
        m = cmdLines.size();
        // 输入的指令为空
        if (m == 0) {
            printFailMessage();
            return;
        }
        currentCommand = 1;
        qOut.clear();
        // 设置输入框不可修改
        ui->cmdTextEdit->setFocusPolicy(Qt::NoFocus);
        ui->currentStepLabel->setText(cmdLines[currentCommand - 1]);
        ui->confirmNextStepButton->setText("下一步");
        doing = true;
    } else {
        if (currentCommand == m + 1) {
            // 如果通关,则更新玩家进度
            if (checkResult()) {
                if (currentLevel != 4)
                    level = currentLevel;
                renderLevelButton(level);
            }
            return;
        }
        // 当前指令
        QString cmd = cmdLines[currentCommand - 1].split(' ')[0];
        // 参数个数
        int argc = cmdLines[currentCommand - 1].split(' ').size() - 1;
        if (cmdSet.contains(cmd)) {
            if (cmd == "inbox") {
                if (argc != 0) {
                    printErrorMessage();
                    return;
                }
                if (qIn.empty()) {
                    checkResult();
                }
                currentBlock = qIn.front();
                qIn.dequeue();
                existCurrentBlock = true;
            } else if (cmd == "outbox") {
                if (existCurrentBlock == false || argc != 0) {
                    printErrorMessage();
                    return;
                }
                qOut.enqueue(currentBlock);
                existCurrentBlock = false;
            } else if (cmd == "add") {
                int x = myToInt(cmdLines[currentCommand - 1].split(' ')[1]);
                if (existCurrentBlock == false || x >= vec.size() || x < 0 || existVec[x] == false || argc != 1) {
                    printErrorMessage();
                    return;
                }
                currentBlock += vec[x];
            } else if (cmd == "sub") {
                int x = myToInt(cmdLines[currentCommand - 1].split(' ')[1]);
                if (existCurrentBlock == false || x >= vec.size() || x < 0 || existVec[x] == false || argc != 1) {
                    printErrorMessage();
                    return;
                }
                currentBlock -= vec[x];
            } else if (cmd == "copyto") {
                int x = myToInt(cmdLines[currentCommand - 1].split(' ')[1]);
                if (existCurrentBlock == false || x >= vec.size() || x < 0 || argc != 1) {
                    printErrorMessage();
                    return;
                }
                vec[x] = currentBlock;
                existVec[x] = true;
            } else if (cmd == "copyfrom") {
                int x = myToInt(cmdLines[currentCommand - 1].split(' ')[1]);
                if (x >= vec.size() || x < 0 || existVec[x] == false || argc != 1) {
                    printErrorMessage();
                    return;
                }
                currentBlock = vec[x];
                existCurrentBlock = true;
            } else if (cmd == "jump") {
                int x = myToInt(cmdLines[currentCommand - 1].split(' ')[1]);
                if (x > m || x <= 0 || argc != 1) {
                    printErrorMessage();
                    return;
                }
                currentCommand = x;
                ui->currentStepLabel->setText(cmdLines[currentCommand - 1]);
                return;
            } else if (cmd == "jumpifzero") {
                int x = myToInt(cmdLines[currentCommand - 1].split(' ')[1]);
                if (existCurrentBlock == false || x > m || x <= 0 || argc != 1) {
                    printErrorMessage();
                    return;
                }
                if (currentBlock == 0) {
                    currentCommand = x;
                    ui->currentStepLabel->setText(cmdLines[currentCommand - 1]);
                    return;
                }
            } else {
                printErrorMessage();
                return;
            }
        } else {
            printErrorMessage();
            return;
        }
        currentCommand++;
        // 如果当前不是最后一步
        if (currentCommand <= m) {
            ui->currentStepLabel->setText(cmdLines[currentCommand - 1]);
        } else { // 否则“下一步”按钮变为“检查”按钮,并且屏幕上显示的“当前指令”为空
            ui->currentStepLabel->clear();
            ui->confirmNextStepButton->setText("检查");
        }
        drawStatus();
    }
}

若还未读入指令(第一次点击 确认 键),函数将读入输入文本框中的全部内容,并去除其中空行与多余的空格,将其分割后存入列表 cmdLines 中,随后将 确认 键的文本改为 下一步

若已经读入了指令,函数将按行进行以下操作:首先检查输入的指令是否在本关卡的可用指令集中,随后调用 myToInt(QString) 函数判断参数是否合法并获取参数;若未出现异常则执行指令,并在最后一条指令执行完成后调用 checkResult() 函数确认关卡是否成功通过。

判断输入的指令是否合法

// 判断一个字符串是否为自然数
int myToInt(QString str) {
    for (auto x : str) {
        // 即:不能出现除数字以外的字符
        if (!x.isDigit())
            return -1;
    }
    return str.toInt();
}

myToInt(QString) 函数用于判断指令的参数是否合法。本次大作业中所有指令的参数最多只有一个,且不存在除数字外的字符(无小数点、无负号),可根据这一点进行判断。

界面设计

程序共有四个界面。进入游戏将显示主界面:

通过此界面可以前往关卡选择界面,设置界面或者退出游戏。

从关卡选择界面可以前往四个关卡,或者返回主界面。

游戏界面显示了关卡的当前状态。玩家可以在页面**的文本框中输入指令。

设置界面可以调整游戏背景音乐的音量。

游戏测试

游戏可以正确执行每条指令,并能够在其出错时识别出来。

inbox

outbox

当前积木不存在时,程序会报错。

add

当前积木不存在时,程序会报错。

目标空地不存在积木时,程序会报错。

sub

当前积木不存在时,程序会报错。

目标空地不存在积木时,程序会报错。

copyto

当前积木不存在时,程序会报错。

目标空地不存在时,程序会报错。

copyfrom

目标空地没有积木时,程序会报错。

目标空地不存在时,程序会报错。

jump

目标指令不存在时,程序会报错。

jumpifzero

当前方块不为0时,程序不会做任何操作。

目标指令不存在时,程序会报错。

当前积木不存在时,程序会报错。

从文件读入指令

本程序可以从一个 .txt 文件中读入指令。

在文本框为空时直接点击确认,则会出现弹窗,可以选择文件。

一个事例是当前目录下的 program.txt 文件:

inbox
copyto 0
inbox
copyto 1
copyfrom 0
sub 1
outbox
copyfrom 1
sub 0
outbox
inbox
copyto 0
inbox
copyto 1
copyfrom 0
sub 1
outbox
copyfrom 1
sub 0
outbox
inbox
copyto 2
sub 2
outbox
inbox
sub 2
outbox
inbox
copyto 0
inbox
copyto 1
copyfrom 0
sub 1
outbox
copyfrom 0
add 1
add 1
add 1
outbox

加载文件后,文件内的指令将会显示在文本框中,可以继续执行。

其他异常情况

出现未定义指令

指令参数错误(为负数)

指令参数错误(不为整数)

指令参数错误(参数数量过多)

除此以外,本程序还可以自动去除玩家输入中的空行以及多余的空格,成功识别出指令及参数。

自由创新关卡

我们所设计的自由创新关卡如下:

若要游玩这一关卡,可以参考当前目录下的事例 extra_level.json,文件内容如下:

{
    "input": [1],
    "output": [2147483647],
    "vacancy": 3,
    "cmd": [0, 1, 2, 3, 4, 5, 6, 7]
}

仓库中的 extra_level.json 即为此例。这一关卡的思路很简单,就是通过累加的方式将 1 变为2147483647。目标过大,直接 add 显然无法解决问题,考虑利用空位存储当前数字,每次将其加倍,然而也需要三十余次加倍的过程。为节省输入指令的量,可以考虑使用 jumpjumpifzero

一个可行的输入指令序列如下:

inbox
copyto 2
copyto 0
copyto 1
add 0
jumpifzero 8
jump 3
copyfrom 1
sub 2
outbox

每次 jump 后,前两块空地,以及当前积木的数字都会加倍,而 2 号空地则保持 1 不变。

这一进程将持续到数字自然溢出,变为 -2147483648

随后,在执行 add 0 后,当前积木将变为 0,满足 jumpifzero 的要求。

最后,通过 copyfrom 1sub 2 将当前积木由 -2147483648 变为 2147483647 后输出,全过程仅需 10 行代码。

小组分工

在本次大作业的开发过程中,熊泽恩负责 70% 的代码以及后续问题的调试,汪宇萌负责 30% 的代码、测试以及 UI 设计。

在整个开发过程中,我们遇到了不少困难与挑战。然而,通过一步步地查找资料、不断测试调试,我们最终摸索出了解决方法,完成了本次大作业的开发。这一经历是十分难忘并且充满教育意义的。我们增进了对应用开发的了解,更熟练地掌握了各类开发工具的使用,也亲身体会到了开发过程中复现问题、调试代码的重要与艰辛。

最后,我们由衷感谢清华大学计算机系 徐明星 老师的指导,以及助教们的答疑。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.