Files
PuzGame/src/game/Puzzle.cpp
2025-03-20 20:18:40 +01:00

624 lines
23 KiB
C++

#include "../gen/Def.h"
#include "Global.h"
#include "Instance.h"
#include "Log.h"
#include "Puzzle.h"
#include "bx/bx.h"
#include "bx/string.h"
#include "imgui.h"
#include <cassert>
#include <cstdio>
namespace
{
static constexpr Generated::PuzPos Dirs[4]{
{-1, 0},
{0, -1},
{0, 1},
{1, 0},
};
Generated::StaticPuzzleData StaticData;
Generated::StaticPuzzleCard InvalidCard;
} // namespace
namespace Puzzle
{
constexpr float UIPuzBoxSize = 26;
using namespace Generated;
void Setup()
{
LOG("Setting up static puzzle data");
LoadStaticPuzzleData();
}
StaticPuzzleData& GetStaticPuzzleData()
{
return StaticData;
}
void LoadStaticPuzzleData()
{
Deserializer ser;
ser.Init("game/data/static/puzzle.dat");
if (ser.ReadT("SPUZ", StaticData))
{
LOG("Successfully loaded static puzzle data!");
}
ser.Finish();
}
void SaveStaticPuzzleData()
{
auto& data = GetStaticPuzzleData();
Serializer ser;
ser.Init("game/data/static/puzzle.dat");
if (ser.WriteT("SPUZ", GetStaticPuzzleData()))
{
LOG("Successfully saved static puzzle data!");
}
ser.Finish();
}
const StaticPuzzleCard& GetCard(StaticPuzzleCardHandle H)
{
if (H.Idx == UINT16_MAX)
{
LOG_ERROR("Invalid static card handle!");
return InvalidCard;
}
return GetStaticPuzzleData().Cards[H.Idx];
}
bool IsValid(StaticPuzzleCardHandle h)
{
return h.Idx != UINT16_MAX;
}
const char* GetShortNodeName(const PuzzleElementType::Enum node)
{
return PuzzleElementType::ShortName[node];
}
uint8_t GetRemainingCount(const PuzzleCardStack& stack)
{
assert(stack.MaxAvailableCount >= stack.UsedCount);
return stack.MaxAvailableCount - stack.UsedCount;
}
PuzzleElementType::Enum GetNodeAt(const PuzzleData& puz, PuzPos pos)
{
assert(pos.X < Puzzle::Config::MaxPuzzleSizeTiles && pos.Y < Puzzle::Config::MaxPuzzleSizeTiles && pos.X >= 0 &&
pos.Y >= 0);
int32_t cardIdxX = pos.X / Puzzle::Config::CardSize;
int32_t cardIdxY = pos.Y / Puzzle::Config::CardSize;
auto& card = puz.PlacedCards[cardIdxY * Puzzle::Config::MaxPuzzleSizeCards + cardIdxX];
int32_t offsetX = pos.X - (cardIdxX * Config::CardSize);
int32_t offsetY = pos.Y - (cardIdxY * Config::CardSize);
if (offsetX >= 0 && offsetX < Puzzle::Config::CardSize && offsetY >= 0 && offsetY < Puzzle::Config::CardSize &&
IsValid(card.RefCard))
{
PuzzleElementType::Enum cardVal = GetCardNodeAt(GetCard(card.RefCard), card.Rotation, offsetX, offsetY);
if (cardVal != PuzzleElementType::None) return cardVal;
}
return puz.BackgroundTiles[pos.Y * Puzzle::Config::MaxPuzzleSizeTiles + pos.X];
}
PuzzleElementType::Enum GetCardNodeAt(const StaticPuzzleCard& card, uint8_t rotation, int8_t x, int8_t y)
{
assert(x >= 0 && x < Puzzle::Config::CardSize);
assert(y >= 0 && y < Puzzle::Config::CardSize);
uint8_t arrX = x;
uint8_t arrY = y;
if (rotation == 1)
{
arrX = y;
arrY = Config::CardSize - x - 1;
}
else if (rotation == 2)
{
arrX = Config::CardSize - x - 1;
arrY = Config::CardSize - y - 1;
}
else if (rotation == 3)
{
arrX = Config::CardSize - y - 1;
arrY = x;
}
return card.Elements[arrY * Puzzle::Config::CardSize + arrX];
}
PuzzleElementType::Enum& EditCardNodeAt(StaticPuzzleCard& card, uint8_t rotation, int8_t x, int8_t y)
{
assert(x >= 0 && x < Puzzle::Config::CardSize);
assert(y >= 0 && y < Puzzle::Config::CardSize);
uint8_t arrX = x;
uint8_t arrY = y;
if (rotation == 1)
{
arrX = y;
arrY = Config::CardSize - x - 1;
}
else if (rotation == 2)
{
arrX = Config::CardSize - x - 1;
arrY = Config::CardSize - y - 1;
}
else if (rotation == 3)
{
arrX = Config::CardSize - y - 1;
arrY = x;
}
return card.Elements[arrY * Puzzle::Config::CardSize + arrX];
}
bool PuzzleSolver::IsPuzzleSolved(const PuzzleData& puzzle)
{
bool IsSolved = true;
for (uint32_t i = 0; i < puzzle.GoalPositionCount; ++i)
{
if (!IsExitSatisfied(puzzle, puzzle.GoalPositions[i]))
{
IsSolved = false;
break;
}
}
return IsSolved;
}
bool PuzzleSolver::IsExitSatisfied(const PuzzleData& puzzle, PuzPos pos)
{
PuzzleElementType::Enum goalType = GetNodeAt(puzzle, pos);
uint32_t currentPositionQueueIdx = 0;
uint32_t positionQueueCount = 0;
PuzPos positionQueue[Puzzle::Config::MaxTilesInPuzzle];
positionQueue[0] = pos;
positionQueueCount++;
while (currentPositionQueueIdx < positionQueueCount)
{
assert(currentPositionQueueIdx < Puzzle::Config::MaxTilesInPuzzle);
PuzPos currentPos = positionQueue[currentPositionQueueIdx];
for (PuzPos dir : Dirs)
{
PuzPos nextPos = currentPos + dir;
if (!(nextPos.X >= 0 && nextPos.Y >= 0 && nextPos.X < puzzle.WidthTiles &&
nextPos.Y < puzzle.HeightTiles))
continue;
if (IsValidGoalConnection(puzzle, nextPos, currentPos, goalType))
{
if (IsValidSource(GetNodeAt(puzzle, nextPos), goalType))
{
return true;
}
bool found = false;
for (int32_t i = 0; i < positionQueueCount; ++i)
{
if (positionQueue[i].X == nextPos.X && positionQueue[i].Y == nextPos.Y)
{
found = true;
break;
}
}
if (!found)
{
positionQueue[positionQueueCount] = nextPos;
positionQueueCount++;
}
}
}
currentPositionQueueIdx++;
}
return false;
}
bool PuzzleSolver::IsValidGoalConnection(const PuzzleData& puzzle,
PuzPos flowFrom,
PuzPos flowTo,
PuzzleElementType::Enum goalType)
{
auto from = GetNodeAt(puzzle, flowFrom);
auto to = GetNodeAt(puzzle, flowTo);
if (goalType == PuzzleElementType::WaterGoal)
{
return from == PuzzleElementType::WaterIn || from == PuzzleElementType::WaterChannel ||
from == PuzzleElementType::WaterGoal || from == PuzzleElementType::Bridge;
}
else if (goalType == PuzzleElementType::ElectricGoal)
{
return from == PuzzleElementType::ElectricIn || from == PuzzleElementType::ElectricGoal ||
from == PuzzleElementType::None;
}
assert(false);
return false;
}
bool PuzzleSolver::IsValidSource(PuzzleElementType::Enum sourceType, PuzzleElementType::Enum goalType)
{
return (sourceType == PuzzleElementType::WaterIn && goalType == PuzzleElementType::WaterGoal) ||
(sourceType == PuzzleElementType::ElectricIn && goalType == PuzzleElementType::ElectricGoal);
}
void DrawCard(const StaticPuzzleCard& card, uint8_t rotation, ImVec2 pos)
{
auto& drawList = *ImGui::GetWindowDrawList();
for (int32_t y = 0; y < Puzzle::Config::CardSize; ++y)
{
for (int32_t x = 0; x < Puzzle::Config::CardSize; ++x)
{
ImGui::SetCursorScreenPos({pos.x + x * UIPuzBoxSize, pos.y + y * UIPuzBoxSize});
ImGui::Text("%s", GetShortNodeName(GetCardNodeAt(card, rotation, x, y)));
}
}
for (int32_t y = 0; y <= Puzzle::Config::CardSize; ++y)
{
ImVec2 linePos = {pos.x, pos.y + y * UIPuzBoxSize};
drawList.AddLine(linePos,
{linePos.x + Puzzle::Config::CardSize * UIPuzBoxSize, linePos.y},
y % Puzzle::Config::CardSize == 0 ? 0xFFFFFFFF : 0x11FFFFFF);
}
for (int32_t x = 0; x <= Puzzle::Config::CardSize; ++x)
{
ImVec2 linePos = {pos.x + x * UIPuzBoxSize, pos.y};
drawList.AddLine(linePos,
{linePos.x, linePos.y + Puzzle::Config::CardSize * UIPuzBoxSize},
x % Puzzle::Config::CardSize == 0 ? 0xFFFFFFFF : 0x11FFFFFF);
}
ImGui::SetCursorScreenPos(pos);
ImGui::InvisibleButton("cardbn",
{Puzzle::Config::CardSize * UIPuzBoxSize, Puzzle::Config::CardSize * UIPuzBoxSize});
}
void WritePuzzleFilePath(char* buf, int32_t bufSize, uint16_t puzID)
{
bx::snprintf(buf, bufSize, "%s/%u.pzl", Puzzle::PuzzleFileDir, puzID);
}
// TODO: targetPos is of type CardPos
bool ReturnPlacedCard(PuzzleData& obj, PuzPos targetPos)
{
auto& placedCard = obj.PlacedCards[targetPos.Y * Config::MaxPuzzleSizeCards + targetPos.X];
if (IsValid(placedCard.RefCard))
{
if (placedCard.IsLocked) return false;
bool found = false;
for (int32_t i = 0; i < obj.AvailableCardCount; ++i)
{
if (obj.AvailableCards[i].RefCard.Idx == placedCard.RefCard.Idx && obj.AvailableCards[i].UsedCount > 0)
{
obj.AvailableCards[i].UsedCount--;
found = true;
break;
}
}
if (!found)
{
LOG_ERROR("Failed to find available card to return placed card of type %u to!", placedCard.RefCard.Idx);
return false;
}
placedCard.RefCard = {};
}
return true;
}
// TODO: targetPos is of type CardPos
bool DragAvailableCardTo(PuzzleData& obj, PuzPos targetPos, int32_t availIdx, uint8_t rotation)
{
if (availIdx >= obj.AvailableCardCount)
{
LOG_ERROR("Invalid drag with avail idx %i!", availIdx);
return false;
}
if (!ReturnPlacedCard(obj, targetPos)) return false;
auto& draggedCard = obj.AvailableCards[availIdx];
draggedCard.UsedCount++;
auto& placedCard = obj.PlacedCards[targetPos.Y * Config::MaxPuzzleSizeCards + targetPos.X];
placedCard.RefCard = draggedCard.RefCard;
placedCard.IsLocked = false;
placedCard.Position = targetPos;
placedCard.Rotation = rotation;
return true;
}
bool RenderDebugUI(PuzzleData& obj)
{
bool dataChanged = false;
uint8_t debugRot = Game::GetInstance().DebugData.DebugCardRotation;
bool isVisible = true;
if (ImGui::Begin("Puzzle", &isVisible))
{
if (ImGui::Button("Delete Puzzle"))
{
char filepath[128]{0};
WritePuzzleFilePath(filepath, sizeof(filepath), obj.ID);
remove(filepath);
obj.ID = UINT16_MAX;
ImGui::End();
return false;
}
ImGui::SameLine();
if (ImGui::Button("Reload"))
{
// TODO
}
int32_t val = obj.ID;
if (ImGui::InputInt("ID", &val))
{
obj.ID = val;
dataChanged = true;
}
dataChanged = ImGui::InputText("Name", obj.PuzzleName, sizeof(obj.PuzzleName)) || dataChanged;
int32_t W = obj.WidthTiles;
int32_t H = obj.HeightTiles;
ImGui::PushID("width");
ImGui::SetNextItemWidth(40);
if (ImGui::DragInt("", &W, 0.3f, 0, Puzzle::Config::MaxPuzzleSizeTiles))
{
obj.WidthTiles = uint8_t(W);
dataChanged = true;
}
ImGui::PopID();
ImGui::SameLine();
ImGui::Text("x");
ImGui::SameLine();
ImGui::SetNextItemWidth(40);
ImGui::PushID("height");
if (ImGui::DragInt("", &H, 0.3f, 0, Puzzle::Config::MaxPuzzleSizeTiles))
{
obj.HeightTiles = uint8_t(H);
dataChanged = true;
}
ImGui::PopID();
ImVec2 puzCursorStart = ImGui::GetCursorScreenPos();
auto& drawList = *ImGui::GetWindowDrawList();
obj.GoalPositionCount = 0;
for (int32_t y = 0; y < obj.HeightTiles; ++y)
{
ImGui::PushID(y);
for (int32_t x = 0; x < obj.WidthTiles; ++x)
{
ImGui::PushID(x);
PuzPos nodePos = {int8_t(x), int8_t(y)};
auto node = GetNodeAt(obj, nodePos);
if (node == PuzzleElementType::WaterGoal)
{
obj.GoalPositions[obj.GoalPositionCount] = nodePos;
obj.GoalPositionCount++;
}
ImVec2 pos = ImVec2{puzCursorStart.x + x * UIPuzBoxSize + 5, puzCursorStart.y + y * UIPuzBoxSize};
ImGui::SetCursorScreenPos(pos);
ImGui::Text("%s", GetShortNodeName(node));
ImGui::SetCursorScreenPos(pos);
if (x % Puzzle::Config::CardSize == 0 && y % Puzzle::Config::CardSize == 0)
{
int32_t cardX = x / Config::CardSize;
int32_t cardY = y / Config::CardSize;
PuzPos cardPos = {int8_t(cardX), int8_t(cardY)};
auto& placedCard = obj.PlacedCards[cardY * Config::MaxPuzzleSizeCards + cardX];
ImGui::InvisibleButton("bn", {UIPuzBoxSize * 2, UIPuzBoxSize * 2});
if (ImGui::IsItemClicked(ImGuiMouseButton_Right))
{
placedCard.Rotation += 1;
if (placedCard.Rotation >= 4) placedCard.Rotation = 0;
dataChanged = true;
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Middle) && IsValid(placedCard.RefCard))
{
if (placedCard.IsLocked)
{
placedCard.IsLocked = false;
placedCard.RefCard = {};
dataChanged = true;
}
else
{
if (!ReturnPlacedCard(obj, cardPos))
{
placedCard.IsLocked = false;
placedCard.RefCard = {};
dataChanged = true;
}
}
}
if (!placedCard.IsLocked && IsValid(placedCard.RefCard))
{
ImVec2 s = {puzCursorStart.x + x * UIPuzBoxSize, puzCursorStart.y + y * UIPuzBoxSize};
drawList.AddRectFilled(s,
{s.x + UIPuzBoxSize * Puzzle::Config::CardSize,
s.y + UIPuzBoxSize * Puzzle::Config::CardSize},
0x33FFFFFF);
}
if (ImGui::BeginDragDropTarget())
{
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("cardtype", 0))
{
uint32_t CardIdx = *reinterpret_cast<uint32_t*>(payload->Data);
placedCard = {};
placedCard.RefCard = {(uint16_t)CardIdx};
placedCard.Rotation = debugRot;
placedCard.Position = cardPos;
placedCard.IsLocked = true;
dataChanged = true;
}
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("availcard", 0))
{
DragAvailableCardTo(obj,
cardPos,
*(int32_t*)payload->Data,
Game::GetInstance().DebugData.DebugCardRotation);
}
ImGui::EndDragDropTarget();
}
}
ImGui::PopID();
}
ImGui::PopID();
}
for (int32_t y = 0; y <= obj.HeightTiles; ++y)
{
ImVec2 linePos = {puzCursorStart.x, puzCursorStart.y + y * UIPuzBoxSize};
drawList.AddLine(linePos,
{linePos.x + obj.WidthTiles * UIPuzBoxSize, linePos.y},
y % Puzzle::Config::CardSize == 0 ? 0xFFFFFFFF : 0x11FFFFFF);
}
for (int32_t x = 0; x <= obj.WidthTiles; ++x)
{
ImVec2 linePos = {puzCursorStart.x + x * UIPuzBoxSize, puzCursorStart.y};
drawList.AddLine(linePos,
{linePos.x, linePos.y + obj.HeightTiles * UIPuzBoxSize},
x % Puzzle::Config::CardSize == 0 ? 0xFFFFFFFF : 0x11FFFFFF);
}
ImVec2 pos = ImVec2{puzCursorStart.x + obj.WidthTiles * UIPuzBoxSize + 5,
puzCursorStart.y + obj.HeightTiles * UIPuzBoxSize};
ImGui::SetCursorScreenPos(pos);
ImGui::NewLine();
ImGui::Separator();
PuzzleSolver solver;
ImGui::Text("Solved: %s", solver.IsPuzzleSolved(obj) ? "yes!" : "no.");
bool bAvailOpen =
ImGui::TreeNodeEx("Available Cards", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed);
if (ImGui::BeginDragDropTarget())
{
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("cardtype", 0))
{
uint32_t CardIdx = *reinterpret_cast<uint32_t*>(payload->Data);
bool found = false;
for (int32_t i = 0; i < obj.AvailableCardCount; ++i)
{
if (obj.AvailableCards[i].RefCard.Idx == CardIdx)
{
obj.AvailableCards[i].MaxAvailableCount++;
found = true;
break;
}
}
if (!found)
{
if (obj.AvailableCardCount == BX_COUNTOF(obj.AvailableCards))
{
LOG_ERROR("Available card limit reached!");
}
else
{
auto& Card = obj.AvailableCards[obj.AvailableCardCount];
Card.RefCard.Idx = CardIdx;
Card.MaxAvailableCount = 1;
obj.AvailableCardCount++;
}
}
dataChanged = true;
}
ImGui::EndDragDropTarget();
}
if (bAvailOpen)
{
ImVec2 startPos = ImGui::GetCursorScreenPos();
for (uint32_t i = 0; i < obj.AvailableCardCount; ++i)
{
ImVec2 localPos = {startPos.x + i * 60.0f, startPos.y};
ImGui::SetCursorScreenPos(localPos);
ImGui::PushID(i);
auto& card = obj.AvailableCards[i];
DrawCard(GetCard(card.RefCard), debugRot, ImGui::GetCursorScreenPos());
int displayCount = card.MaxAvailableCount - card.UsedCount;
if (displayCount > 0 && ImGui::BeginDragDropSource())
{
ImGui::SetDragDropPayload("availcard", &i, sizeof(i));
DrawCard(GetCard(card.RefCard), debugRot, ImGui::GetCursorScreenPos());
ImGui::EndDragDropSource();
}
ImGui::SetCursorScreenPos({localPos.x, localPos.y + 55.0f});
ImGui::SetNextItemWidth(35);
ImGui::PushID("carc");
if (ImGui::DragInt("", &displayCount, 0.25f))
{
int diff = displayCount - (card.MaxAvailableCount - card.UsedCount);
card.MaxAvailableCount = bx::max(0, card.MaxAvailableCount + diff);
dataChanged = true;
}
ImGui::PopID();
ImGui::SameLine(0, 3);
if (ImGui::Button("x"))
{
if (i < obj.AvailableCardCount - 1)
{
obj.AvailableCards[i] = obj.AvailableCards[obj.AvailableCardCount - 1];
}
obj.AvailableCardCount--;
}
ImGui::PopID();
}
ImGui::TreePop();
}
}
ImGui::End();
if (dataChanged)
{
char path[128]{0};
WritePuzzleFilePath(path, sizeof(path), obj.ID);
Serializer ser;
ser.Init(path);
if (ser.WriteT("PZZL", obj))
{
LOG("Saved to %s", path);
}
else
{
LOG_ERROR("Failed to save to %s", path);
}
ser.Finish();
}
return isVisible;
}
} // namespace Puzzle
namespace Generated
{
PuzPos operator+=(PuzPos lhs, const PuzPos& rhs)
{
lhs.X += rhs.X;
lhs.Y += rhs.Y;
return lhs;
}
PuzPos operator+(PuzPos lhs, const PuzPos& rhs)
{
PuzPos res = lhs;
res.X += rhs.X;
res.Y += rhs.Y;
return res;
}
PuzPos operator-=(PuzPos lhs, const PuzPos& rhs)
{
lhs.X -= rhs.X;
lhs.Y -= rhs.Y;
return lhs;
}
PuzPos operator-(PuzPos lhs, const PuzPos& rhs)
{
PuzPos res = lhs;
res.X += rhs.X;
res.Y += rhs.Y;
return res;
}
} // namespace Generated