FTowerX
A few years ago, I was developing a program for remotely controlling a signal generator. I wanted to create a simple user interface and have maximum portability. The program was intended as an in-house testing tool, so I didn't want to invest much time in it. I ended up using Qt.
Later, I came across a fantastic library for implementing text-based UIs in terminal applications called FTXUI (Functional Terminal (X) User interface). This weekend, I found the opportunity to explore that library and its capabilities through developing my favorite game: Tower of Hanoi.
This is an open-source library, and I suggest visiting their repository. There, you can find a more comprehensive showcase of its functionality. However, in this article, I explain my implementation of the game.
I started with defining the Disk class:
class Disk {
public:
Disk(size_t size) : m_size{size} {}
size_t getSize() const { return m_size; }
private:
size_t m_size{};
};
bool operator<(const Disk &lhs, const Disk &rhs) {
return lhs.getSize() < rhs.getSize();
}
After that, I continued creating a Uniform Stack, where you can only push elements smaller than the previous element.
template <typename T, class Compare = std::less<>> class UniformStack {
public:
T pop() {
if (m_stack.empty()) {
throw std::out_of_range("calling pop on an empty UniformStack");
}
auto var{std::move(m_stack.back())};
m_stack.pop_back();
return var;
};
std::optional<T> push(T &&var) {
if (!m_stack.empty() && !Compare{}(var, m_stack.back()))
return std::move(var);
m_stack.push_back(std::move(var));
return {};
};
...
};
I used three of these stacks to create the GameOfTOH, where you select a disk and if possible, move it to another stack.
void selectOrMoveToTower(TOWERS index) {
if (index == TOWER_END) {
m_srcTower = nullptr;
return;
}
if (m_srcTower && moveDisk(m_srcTower, &m_towers[index])) {
m_srcTower = nullptr;
} else {
if (!m_towers[index].empty()) {
m_srcTower = &m_towers[index];
}
}
}
bool moveDisk(Stack *src, Stack *dst) {
if (!src->empty()) {
auto disk{src->pop()};
std::optional<std::unique_ptr<Disk>> rejectedDisk{};
if (rejectedDisk = dst->push(std::move(disk))) {
src->push(std::move(*std::move(rejectedDisk)));
return false;
}
return true;
}
return false;
}
Now, I needed a way to represent the stacks using FTXUI. I wrote a function that takes a stack and returns an FTXUI::vbox consisting of text elements where I used background color to present disks.
std::vector<Element> disks{};
disks.push_back(filler());
for (int i = stack.size() - 1; i >= 0; i--) {
disks.push_back(text(std::string(stack[i]->getSize() * 2, ' ')) | bgcolor(colorMap.at(stack[i]->getSize() % 10)) | center);
if (isSrc && disks.size() == 2) {
disks.push_back(filler());
}
}
return vbox(disks);
In the main loop, I draw each stack next to another and also capture keyboard strokes to move the disks between them:
return vbox(
hbox(printTower(got.getTower(GameOfTOH::TOWER_1),
got.isSourceTower(GameOfTOH::TOWER_1)) |
flex | ftxui::size(WIDTH, EQUAL, Terminal::Size().dimx),
separator(),
printTower(got.getTower(GameOfTOH::TOWER_2),
got.isSourceTower(GameOfTOH::TOWER_2)) |
flex | ftxui::size(WIDTH, EQUAL, Terminal::Size().dimx),
separator(),
printTower(got.getTower(GameOfTOH::TOWER_3),
got.isSourceTower(GameOfTOH::TOWER_3)) |
flex | ftxui::size(WIDTH, EQUAL, Terminal::Size().dimx)) |
border
});
auto component = CatchEvent(renderer, [&](Event event) {
if (event == Event::Character('q')) {
screen.ExitLoopClosure()();
return true;
}
if (event == Event::ArrowLeft) {
got.selectOrMoveToTower(GameOfTOH::TOWER_1);
}
if (event == Event::ArrowDown) {
got.selectOrMoveToTower(GameOfTOH::TOWER_2);
}
if (event == Event::ArrowRight) {
got.selectOrMoveToTower(GameOfTOH::TOWER_3);
}
if (event == Event::Escape) {
got.selectOrMoveToTower(GameOfTOH::TOWER_END);
}
return false;
});
As simple as that.
Working with this library was a joy, and it offers much more than I have shown here. I hope you find it useful. You can review my code at: https://github.com/MhmRhm/FTowerX
Thank you, Mohammad. It was helpful.