From e928dda4b31b5ace011a4d9354107a846e769ea4 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Wed, 5 Mar 2025 20:21:24 -0500 Subject: [PATCH] Squashed: grid-entity-integration partial features for 7DRL 2025 deployment This squash commit includes changes from April 21st through 28th, 2024, and the past 3 days of work at 7DRL. Rather than resume my feature branch work, I made minor changes to safe the C++ functionality and wrote workarounds in Python. I'm very likely to delete this commit from history by rolling master back to the previous commit, and squash merging a finished feature branch. --- assets/kenney_TD_MR_IP.png | Bin 0 -> 678330 bytes src/GameEngine.cpp | 9 +- src/PyVector.cpp | 13 ++ src/PyVector.h | 2 + src/UICaption.cpp | 29 +++- src/UICaption.h | 2 +- src/UIDrawable.cpp | 3 +- src/UIDrawable.h | 3 +- src/UIEntity.cpp | 49 +++++- src/UIEntity.h | 5 +- src/UIFrame.cpp | 7 +- src/UIFrame.h | 2 +- src/UIGrid.cpp | 48 +++--- src/UIGrid.h | 2 +- src/UISprite.cpp | 4 +- src/UISprite.h | 4 +- src/scripts/cos_entities.py | 203 ++++++++++++++++++++++++ src/scripts/cos_level.py | 195 +++++++++++++++++++++++ src/scripts/cos_play.py | 300 ------------------------------------ src/scripts/cos_tiles.py | 223 +++++++++++++++++++++++++++ src/scripts/game.py | 141 +++++++++++------ src/scripts/game_old.py | 221 -------------------------- 22 files changed, 843 insertions(+), 622 deletions(-) create mode 100644 assets/kenney_TD_MR_IP.png create mode 100644 src/scripts/cos_entities.py create mode 100644 src/scripts/cos_level.py delete mode 100644 src/scripts/cos_play.py create mode 100644 src/scripts/cos_tiles.py delete mode 100644 src/scripts/game_old.py diff --git a/assets/kenney_TD_MR_IP.png b/assets/kenney_TD_MR_IP.png new file mode 100644 index 0000000000000000000000000000000000000000..600c8d94eb0d24c6b2814cfae5442905fdca4039 GIT binary patch literal 678330 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, 0); + int err = init(obj, args, NULL); + if (err) { + Py_DECREF(obj); + return NULL; + } + return obj; +} diff --git a/src/PyVector.h b/src/PyVector.h index 6ea0622..f678ad6 100644 --- a/src/PyVector.h +++ b/src/PyVector.h @@ -1,6 +1,7 @@ #pragma once #include "Common.h" #include "Python.h" +#include "McRFPy_API.h" typedef struct { PyObject_HEAD @@ -22,6 +23,7 @@ public: static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); static PyObject* get_member(PyObject*, void*); static int set_member(PyObject*, PyObject*, void*); + static PyVectorObject* from_arg(PyObject*); static PyGetSetDef getsetters[]; }; diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 9001333..d960c25 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -13,10 +13,11 @@ UIDrawable* UICaption::click_at(sf::Vector2f point) return NULL; } -void UICaption::render(sf::Vector2f offset) +void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target) { text.move(offset); - Resources::game->getWindow().draw(text); + //Resources::game->getWindow().draw(text); + target.draw(text); text.move(-offset); } @@ -222,17 +223,30 @@ PyObject* UICaption::repr(PyUICaptionObject* self) int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) { using namespace mcrfpydef; - static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", nullptr }; - float x = 0.0f, y = 0.0f, outline = 0.0f; + // Constructor switch to Vector position + //static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", nullptr }; + //float x = 0.0f, y = 0.0f, outline = 0.0f; + static const char* keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", nullptr }; + PyObject* pos; + float outline = 0.0f; char* text; PyObject* font=NULL, *fill_color=NULL, *outline_color=NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf", - const_cast(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline)) + //if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf", + // const_cast(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|zOOOf", + const_cast(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline)) { return -1; } - + + PyVectorObject* pos_result = PyVector::from_arg(pos); + if (!pos_result) + { + PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); + return -1; + } + self->data->text.setPosition(pos_result->data); // check types for font, fill_color, outline_color std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl; @@ -251,7 +265,6 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) //self->data->text.setFont(Resources::game->getFont()); } - self->data->text.setPosition(sf::Vector2f(x, y)); self->data->text.setString((std::string)text); self->data->text.setOutlineThickness(outline); if (fill_color) { diff --git a/src/UICaption.h b/src/UICaption.h index bf01a96..2dfdc17 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -7,7 +7,7 @@ class UICaption: public UIDrawable { public: sf::Text text; - void render(sf::Vector2f) override final; + void render(sf::Vector2f, sf::RenderTarget&) override final; PyObjectsEnum derived_type() override final; virtual UIDrawable* click_at(sf::Vector2f point) override final; diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index a710095..693d5f6 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -3,6 +3,7 @@ #include "UICaption.h" #include "UISprite.h" #include "UIGrid.h" +#include "GameEngine.h" UIDrawable::UIDrawable() { click_callable = NULL; } @@ -13,7 +14,7 @@ void UIDrawable::click_unregister() void UIDrawable::render() { - render(sf::Vector2f()); + render(sf::Vector2f(), Resources::game->getWindow()); } PyObject* UIDrawable::get_click(PyObject* self, void* closure) { diff --git a/src/UIDrawable.h b/src/UIDrawable.h index 4e636c5..595fc93 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -28,7 +28,8 @@ class UIDrawable { public: void render(); - virtual void render(sf::Vector2f) = 0; + //virtual void render(sf::Vector2f) = 0; + virtual void render(sf::Vector2f, sf::RenderTarget&) = 0; virtual PyObjectsEnum derived_type() = 0; // Mouse input handling - callable object, methods to find event's destination diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 01ebd83..db8073a 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -34,18 +34,30 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) { } int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr }; - float x = 0.0f, y = 0.0f, scale = 1.0f; + //static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr }; + //float x = 0.0f, y = 0.0f, scale = 1.0f; + static const char* keywords[] = { "pos", "texture", "sprite_index", "grid", nullptr }; + PyObject* pos; + float scale = 1.0f; int sprite_index = -1; PyObject* texture = NULL; PyObject* grid = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O", - const_cast(keywords), &x, &y, &texture, &sprite_index, &grid)) + //if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O", + // const_cast(keywords), &x, &y, &texture, &sprite_index, &grid)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi|O", + const_cast(keywords), &pos, &texture, &sprite_index, &grid)) { return -1; } + PyVectorObject* pos_result = PyVector::from_arg(pos); + if (!pos_result) + { + PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); + return -1; + } + // check types for texture // // Set Texture @@ -75,7 +87,7 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { // TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers self->data->sprite = UISprite(pytexture->data, sprite_index, sf::Vector2f(0,0), 1.0); - self->data->position = sf::Vector2f(x, y); + self->data->position = pos_result->data; if (grid != NULL) { PyUIGridObject* pygrid = (PyUIGridObject*)grid; self->data->grid = pygrid->data; @@ -95,6 +107,10 @@ PyObject* sfVector2f_to_PyObject(sf::Vector2f vector) { return Py_BuildValue("(ff)", vector.x, vector.y); } +PyObject* sfVector2i_to_PyObject(sf::Vector2i vector) { + return Py_BuildValue("(ii)", vector.x, vector.y); +} + sf::Vector2f PyObject_to_sfVector2f(PyObject* obj) { float x, y; if (!PyArg_ParseTuple(obj, "ff", &x, &y)) { @@ -103,6 +119,14 @@ sf::Vector2f PyObject_to_sfVector2f(PyObject* obj) { return sf::Vector2f(x, y); } +sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) { + int x, y; + if (!PyArg_ParseTuple(obj, "ii", &x, &y)) { + return sf::Vector2i(); // TODO / reconsider this default: Return default vector on parse error + } + return sf::Vector2i(x, y); +} + // TODO - deprecate / remove this helper PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) { return PyObject_New(PyObject, (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState")); @@ -125,11 +149,19 @@ PyObject* UIGridPointStateVector_to_PyList(const std::vector& } PyObject* UIEntity::get_position(PyUIEntityObject* self, void* closure) { - return sfVector2f_to_PyObject(self->data->position); + if (reinterpret_cast(closure) == 0) { + return sfVector2f_to_PyObject(self->data->position); + } else { + return sfVector2i_to_PyObject(self->data->collision_pos); + } } int UIEntity::set_position(PyUIEntityObject* self, PyObject* value, void* closure) { - self->data->position = PyObject_to_sfVector2f(value); + if (reinterpret_cast(closure) == 0) { + self->data->position = PyObject_to_sfVector2f(value); + } else { + self->data->collision_pos = PyObject_to_sfVector2i(value); + } return 0; } @@ -158,7 +190,8 @@ PyMethodDef UIEntity::methods[] = { }; PyGetSetDef UIEntity::getsetters[] = { - {"position", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position", NULL}, + {"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (graphically)", (void*)0}, + {"pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (integer grid coordinates)", (void*)1}, {"gridstate", (getter)UIEntity::get_gridstate, NULL, "Grid point states for the entity", NULL}, {"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite number (index) on the texture on the display", NULL}, {NULL} /* Sentinel */ diff --git a/src/UIEntity.h b/src/UIEntity.h index b5050ff..042a933 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -40,7 +40,8 @@ public: std::vector gridstate; UISprite sprite; sf::Vector2f position; //(x,y) in grid coordinates; float for animation - void render(sf::Vector2f); //override final; + sf::Vector2i collision_pos; //(x, y) in grid coordinates: int for collision + //void render(sf::Vector2f); //override final; UIEntity(); UIEntity(UIGrid&); @@ -65,7 +66,7 @@ namespace mcrfpydef { .tp_basicsize = sizeof(PyUIEntityObject), .tp_itemsize = 0, .tp_repr = (reprfunc)UIEntity::repr, - .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = "UIEntity objects", .tp_methods = UIEntity::methods, .tp_getset = UIEntity::getsetters, diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 42adc47..f382127 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -44,14 +44,15 @@ PyObjectsEnum UIFrame::derived_type() return PyObjectsEnum::UIFRAME; } -void UIFrame::render(sf::Vector2f offset) +void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) { box.move(offset); - Resources::game->getWindow().draw(box); + //Resources::game->getWindow().draw(box); + target.draw(box); box.move(-offset); for (auto drawable : *children) { - drawable->render(offset + box.getPosition()); + drawable->render(offset + box.getPosition(), target); } } diff --git a/src/UIFrame.h b/src/UIFrame.h index 5875ad6..9cd5d10 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -28,7 +28,7 @@ public: sf::RectangleShape box; float outline; std::shared_ptr>> children; - void render(sf::Vector2f) override final; + void render(sf::Vector2f, sf::RenderTarget&) override final; void move(sf::Vector2f); PyObjectsEnum derived_type() override final; virtual UIDrawable* click_at(sf::Vector2f point) override final; diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 15ad0a7..2d95120 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -32,9 +32,9 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _x void UIGrid::update() {} -void UIGrid::render(sf::Vector2f) +void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) { - output.setPosition(box.getPosition()); // output sprite can move; update position when drawing + output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing // output size can change; update size when drawing output.setTextureRect( sf::IntRect(0, 0, @@ -172,7 +172,8 @@ void UIGrid::render(sf::Vector2f) // render to window renderTexture.display(); - Resources::game->getWindow().draw(output); + //Resources::game->getWindow().draw(output); + target.draw(output); } @@ -204,12 +205,28 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { int grid_x, grid_y; PyObject* textureObj; - float box_x, box_y, box_w, box_h; + //float box_x, box_y, box_w, box_h; + PyObject* pos, *size; - if (!PyArg_ParseTuple(args, "iiOffff", &grid_x, &grid_y, &textureObj, &box_x, &box_y, &box_w, &box_h)) { + //if (!PyArg_ParseTuple(args, "iiOffff", &grid_x, &grid_y, &textureObj, &box_x, &box_y, &box_w, &box_h)) { + if (!PyArg_ParseTuple(args, "iiOOO", &grid_x, &grid_y, &textureObj, &pos, &size)) { return -1; // If parsing fails, return an error } + PyVectorObject* pos_result = PyVector::from_arg(pos); + if (!pos_result) + { + PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); + return -1; + } + + PyVectorObject* size_result = PyVector::from_arg(size); + if (!size_result) + { + PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); + return -1; + } + // Convert PyObject texture to IndexTexture* // This requires the texture object to have been initialized similar to UISprite's texture handling @@ -224,8 +241,9 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { // Initialize UIGrid //self->data = new UIGrid(grid_x, grid_y, texture, sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h)); - self->data = std::make_shared(grid_x, grid_y, pyTexture->data, - sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h)); + //self->data = std::make_shared(grid_x, grid_y, pyTexture->data, + // sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h)); + self->data = std::make_shared(grid_x, grid_y, pyTexture->data, pos_result->data, size_result->data); return 0; // Success } @@ -423,22 +441,6 @@ PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure) PyObject* UIGrid::repr(PyUIGridObject* self) { - -// if (member_ptr == 0) // x -// self->data->box.setPosition(val, self->data->box.getPosition().y); -// else if (member_ptr == 1) // y -// self->data->box.setPosition(self->data->box.getPosition().x, val); -// else if (member_ptr == 2) // w -// self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y)); -// else if (member_ptr == 3) // h -// self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val)); -// else if (member_ptr == 4) // center_x -// self->data->center_x = val; -// else if (member_ptr == 5) // center_y -// self->data->center_y = val; -// else if (member_ptr == 6) // zoom -// self->data->zoom = val; - std::ostringstream ss; if (!self->data) ss << ""; else { diff --git a/src/UIGrid.h b/src/UIGrid.h index bb09152..625af98 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -26,7 +26,7 @@ public: //UIGrid(int, int, IndexTexture*, float, float, float, float); UIGrid(int, int, std::shared_ptr, sf::Vector2f, sf::Vector2f); void update(); - void render(sf::Vector2f) override final; + void render(sf::Vector2f, sf::RenderTarget&) override final; UIGridPoint& at(int, int); PyObjectsEnum derived_type() override final; //void setSprite(int); diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 1f3a214..1441753 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -18,14 +18,16 @@ UISprite::UISprite(std::shared_ptr _ptex, int _sprite_index, sf::Vect sprite = ptex->sprite(sprite_index, _pos, sf::Vector2f(_scale, _scale)); } +/* void UISprite::render(sf::Vector2f offset) { sprite.move(offset); Resources::game->getWindow().draw(sprite); sprite.move(-offset); } +*/ -void UISprite::render(sf::Vector2f offset, sf::RenderTexture& target) +void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target) { sprite.move(offset); target.draw(sprite); diff --git a/src/UISprite.h b/src/UISprite.h index 917eec5..a831efd 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -25,10 +25,10 @@ public: UISprite(); UISprite(std::shared_ptr, int, sf::Vector2f, float); void update(); - void render(sf::Vector2f) override final; + void render(sf::Vector2f, sf::RenderTarget&) override final; virtual UIDrawable* click_at(sf::Vector2f point) override final; - void render(sf::Vector2f, sf::RenderTexture&); + //void render(sf::Vector2f, sf::RenderTexture&); void setPosition(sf::Vector2f); sf::Vector2f getPosition(); diff --git a/src/scripts/cos_entities.py b/src/scripts/cos_entities.py new file mode 100644 index 0000000..ed916f7 --- /dev/null +++ b/src/scripts/cos_entities.py @@ -0,0 +1,203 @@ +import mcrfpy +#t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) +t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) +#def iterable_entities(grid): +# """Workaround for UIEntityCollection bug; see issue #72""" +# entities = [] +# for i in range(len(grid.entities)): +# entities.append(grid.entities[i]) +# return entities + +class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bugs workarounds + def __init__(self, g:mcrfpy.Grid, x=0, y=0, sprite_num=86, *, game): + #self.e = mcrfpy.Entity((x, y), t, sprite_num) + #super().__init__((x, y), t, sprite_num) + self._entity = mcrfpy.Entity((x, y), t, sprite_num) + #grid.entities.append(self.e) + self.grid = g + #g.entities.append(self._entity) + self.game = game + self.game.add_entity(self) + + ## Wrapping mcfrpy.Entity properties to emulate derived class... see issue #76 + @property + def draw_pos(self): + return self._entity.draw_pos + + @draw_pos.setter + def draw_pos(self, value): + self._entity.draw_pos = value + + @property + def sprite_number(self): + return self._entity.sprite_number + + @sprite_number.setter + def sprite_number(self, value): + self._entity.sprite_number = value + + def __repr__(self): + return f"" + + def die(self): + # ugly workaround! grid.entities isn't really iterable (segfaults) + for i in range(len(self.grid.entities)): + e = self.grid.entities[i] + if e == self._entity: + #if e == self: + self.grid.entities.remove(i) + break + else: + print(f"!!! {self!r} wasn't removed from grid on call to die()") + + def bump(self, other, dx, dy, test=False): + raise NotImplementedError + + def do_move(self, tx, ty): + """Base class method to move this entity + Assumes try_move succeeded, for everyone! + from: self._entity.draw_pos + to: (tx, ty) + calls ev_exit for every entity at (draw_pos) + calls ev_enter for every entity at (tx, ty) + """ + old_pos = self.draw_pos + self.draw_pos = (tx, ty) + for e in self.game.entities: + if e is self: continue + if e.draw_pos == old_pos: e.ev_exit(self) + for e in self.game.entities: + if e is self: continue + if e.draw_pos == (tx, ty): e.ev_enter(self) + + + def ev_enter(self, other): + pass + + def ev_exit(self, other): + pass + + def try_move(self, dx, dy, test=False): + x_max, y_max = self.grid.grid_size + tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + #for e in iterable_entities(self.grid): + + # sorting entities to test against the boulder instead of the button when they overlap. + for e in sorted(self.game.entities, key = lambda i: i.draw_order, reverse = True): + if e.draw_pos == (tx, ty): + #print(f"bumping {e}") + return e.bump(self, dx, dy) + + if tx < 0 or tx >= x_max: + return False + if ty < 0 or ty >= y_max: + return False + if self.grid.at((tx, ty)).walkable == True: + if not test: + #self.draw_pos = (tx, ty) + self.do_move(tx, ty) + return True + else: + #print("Bonk") + return False + + def _relative_move(self, dx, dy): + tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + #self.draw_pos = (tx, ty) + self.do_move(tx, ty) + +class PlayerEntity(COSEntity): + def __init__(self, *, game): + #print(f"spawn at origin") + self.draw_order = 10 + super().__init__(game.grid, 0, 0, sprite_num=84, game=game) + + def respawn(self, avoid=None): + # find spawn point + x_max, y_max = g.size + spawn_points = [] + for x in range(x_max): + for y in range(y_max): + if g.at((x, y)).walkable: + spawn_points.append((x, y)) + random.shuffle(spawn_points) + ## TODO - find other entities to avoid spawning on top of + for spawn in spawn_points: + for e in avoid or []: + if e.draw_pos == spawn: break + else: + break + self.draw_pos = spawn + + def __repr__(self): + return f"" + + +class BoulderEntity(COSEntity): + def __init__(self, x, y, *, game): + self.draw_order = 8 + super().__init__(game.grid, x, y, 66, game=game) + + def bump(self, other, dx, dy, test=False): + if type(other) == BoulderEntity: + #print("Boulders can't push boulders") + return False + #tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) + tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + # Is the boulder blocked the same direction as the bumper? If not, let's both move + old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + if self.try_move(dx, dy, test=test): + if not test: + other.do_move(*old_pos) + #other.draw_pos = old_pos + return True + +class ButtonEntity(COSEntity): + def __init__(self, x, y, exit_entity, *, game): + self.draw_order = 1 + super().__init__(game.grid, x, y, 42, game=game) + self.exit = exit_entity + + def ev_enter(self, other): + print("Button makes a satisfying click!") + self.exit.unlock() + + def ev_exit(self, other): + print("Button makes a disappointing click.") + self.exit.lock() + + def bump(self, other, dx, dy, test=False): + #if type(other) == BoulderEntity: + # self.exit.unlock() + # TODO: unlock, and then lock again, when player steps on/off + if not test: + other._relative_move(dx, dy) + return True + +class ExitEntity(COSEntity): + def __init__(self, x, y, bx, by, *, game): + self.draw_order = 2 + super().__init__(game.grid, x, y, 45, game=game) + self.my_button = ButtonEntity(bx, by, self, game=game) + self.unlocked = False + #global cos_entities + #cos_entities.append(self.my_button) + + def unlock(self): + self.sprite_number = 21 + self.unlocked = True + + def lock(self): + self.sprite_number = 45 + self.unlocked = False + + def bump(self, other, dx, dy, test=False): + if type(other) == BoulderEntity: + return False + if self.unlocked: + if not test: + other._relative_move(dx, dy) + #TODO - player go down a level logic + if type(other) == PlayerEntity: + self.game.create_level(self.game.depth + 1) + self.game.swap_level(self.game.level, self.game.spawn_point) diff --git a/src/scripts/cos_level.py b/src/scripts/cos_level.py new file mode 100644 index 0000000..32392b0 --- /dev/null +++ b/src/scripts/cos_level.py @@ -0,0 +1,195 @@ +import random +import mcrfpy +import cos_tiles as ct + +t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) + +def binary_space_partition(x, y, w, h): + d = random.choices(["vert", "horiz"], weights=[w/(w+h), h/(w+h)])[0] + split = random.randint(30, 70) / 100.0 + if d == "vert": + coord = int(w * split) + return (x, y, coord, h), (x+coord, y, w-coord, h) + else: # horizontal + coord = int(h * split) + return (x, y, w, coord), (x, y+coord, w, h-coord) + +room_area = lambda x, y, w, h: w * h + +class BinaryRoomNode: + def __init__(self, xywh): + self.data = xywh + self.left = None + self.right = None + + def __repr__(self): + return f"" + + def split(self): + new_data = binary_space_partition(*self.data) + self.left = BinaryRoomNode(new_data[0]) + self.right = BinaryRoomNode(new_data[1]) + + def walk(self): + if self.left and self.right: + return self.left.walk() + self.right.walk() + return [self] + +class RoomGraph: + def __init__(self, xywh): + self.root = BinaryRoomNode(xywh) + + def __repr__(self): + return f"" + + def walk(self): + w = self.root.walk() if self.root else [] + #print(w) + return w + +def room_coord(room, margin=0): + x, y, w, h = room.data + w -= 1 + h -= 1 + margin += 1 + x += margin + y += margin + w -= margin + h -= margin + if w < 0: w = 0 + if h < 0: h = 0 + tx = x if w==0 else random.randint(x, x+w) + ty = y if h==0 else random.randint(y, y+h) + return (tx, ty) + +class Level: + def __init__(self, width, height): + self.width = width + self.height = height + #self.graph = [(0, 0, width, height)] + self.graph = RoomGraph( (0, 0, width, height) ) + self.grid = mcrfpy.Grid(width, height, t, (10, 10), (1014, 758)) + self.highlighted = -1 #debug view feature + + def fill(self, xywh, highlight = False): + if highlight: + ts = 0 + else: + ts = room_area(*xywh) % 131 + X, Y, W, H = xywh + for x in range(X, X+W): + for y in range(Y, Y+H): + self.grid.at((x, y)).tilesprite = ts + + def highlight(self, delta): + rooms = self.graph.walk() + if self.highlighted < len(rooms): + print(f"reset {self.highlighted}") + self.fill(rooms[self.highlighted].data) # reset + self.highlighted += delta + print(f"highlight {self.highlighted}") + self.highlighted = self.highlighted % len(rooms) + self.fill(rooms[self.highlighted].data, highlight = True) + + def reset(self): + self.graph = RoomGraph( (0, 0, self.width, self.height) ) + for x in range(self.width): + for y in range(self.height): + self.grid.at((x, y)).walkable = True + self.grid.at((x, y)).transparent = True + self.grid.at((x, y)).tilesprite = 0 #random.choice([40, 28]) + + def split(self, single=False): + if single: + areas = {g.data: room_area(*g.data) for g in self.graph.walk()} + largest = sorted(self.graph.walk(), key=lambda g: areas[g.data])[-1] + largest.split() + else: + for room in self.graph.walk(): room.split() + self.fill_rooms() + + def fill_rooms(self, features=None): + rooms = self.graph.walk() + print(f"rooms: {len(rooms)}") + for i, g in enumerate(rooms): + X, Y, W, H = g.data + #c = [random.randint(0, 255) for _ in range(3)] + ts = room_area(*g.data) % 131 + i # modulo - consistent tile pick + for x in range(X, X+W): + for y in range(Y, Y+H): + self.grid.at((x, y)).tilesprite = ts + + def wall_rooms(self): + rooms = self.graph.walk() + for g in rooms: + #if random.random() > 0.66: continue + X, Y, W, H = g.data + for x in range(X, X+W): + self.grid.at((x, Y)).walkable = False + #self.grid.at((x, Y+H-1)).walkable = False + for y in range(Y, Y+H): + self.grid.at((X, y)).walkable = False + #self.grid.at((X+W-1, y)).walkable = False + # boundary of entire level + for x in range(0, self.width): + # self.grid.at((x, 0)).walkable = False + self.grid.at((x, self.height-1)).walkable = False + for y in range(0, self.height): + # self.grid.at((0, y)).walkable = False + self.grid.at((self.width-1, y)).walkable = False + + def generate(self, target_rooms = 5, features=None): + if features is None: + shuffled = ["boulder", "button"] + random.shuffle(shuffled) + features = ["spawn"] + shuffled + ["exit", "treasure"] + # Binary space partition to reach given # of rooms + self.reset() + while len(self.graph.walk()) < target_rooms: + self.split(single=len(self.graph.walk()) > target_rooms * .5) + + # Player path planning + #self.fill_rooms() + self.wall_rooms() + rooms = self.graph.walk() + feature_coords = {} + prev_room = None + for room in rooms: + if not features: break + f = features.pop(0) + feature_coords[f] = room_coord(room, margin=4 if f in ("boulder",) else 1) + + ## Hallway generation + # plow an inelegant path + if prev_room: + start = room_coord(prev_room, margin=2) + end = room_coord(room, margin=2) + # get x1,y1 and x2,y2 coordinates: top left and bottom right points on the rect formed by two random points, one from each of the 2 rooms + x1 = min([start[0], end[0]]) + x2 = max([start[0], end[0]]) + dw = x2 - x1 + y1 = min([start[1], end[1]]) + y2 = max([start[1], end[1]]) + dh = y2 - y1 + #print(x1, y1, x2, y2, dw, dh) + + # random: top left or bottom right as the corner between the paths + tx, ty = (x1, y1) if random.random() >= 0.5 else (x2, y2) + + for x in range(x1, x1+dw): + self.grid.at((x, ty)).walkable = True + #self.grid.at((x, ty)).color = (255, 0, 0) + #self.grid.at((x, ty)).tilesprite = -1 + for y in range(y1, y1+dh): + self.grid.at((tx, y)).walkable = True + #self.grid.at((tx, y)).color = (0, 255, 0) + #self.grid.at((tx, y)).tilesprite = -1 + prev_room = room + + + # Tile painting + possibilities = None + while possibilities or possibilities is None: + possibilities = ct.wfc_pass(self.grid, possibilities) + + return feature_coords diff --git a/src/scripts/cos_play.py b/src/scripts/cos_play.py deleted file mode 100644 index 5cc9ffd..0000000 --- a/src/scripts/cos_play.py +++ /dev/null @@ -1,300 +0,0 @@ -import mcrfpy -mcrfpy.createScene("play") -ui = mcrfpy.sceneUI("play") -t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) # 12, 11) -font = mcrfpy.Font("assets/JetbrainsMono.ttf") - -frame_color = (64, 64, 128) - -grid = mcrfpy.Grid(20, 15, t, 10, 10, 800, 595) -grid.zoom = 2.0 -entity_frame = mcrfpy.Frame(815, 10, 194, 595, fill_color = frame_color) -inventory_frame = mcrfpy.Frame(10, 610, 800, 143, fill_color = frame_color) -stats_frame = mcrfpy.Frame(815, 610, 194, 143, fill_color = frame_color) - -begin_btn = mcrfpy.Frame(350,250,100,100, fill_color = (255,0,0)) -begin_btn.children.append(mcrfpy.Caption(5, 5, "Begin", font)) -def cos_keys(key, state): - if key == 'M' and state == 'start': - mapgen() - elif state == "end": return - elif key == "W": - player.move("N") - elif key == "A": - player.move("W") - elif key == "S": - player.move("S") - elif key == "D": - player.move("E") - - -def cos_init(*args): - if args[3] != "start": return - mcrfpy.keypressScene(cos_keys) - ui.remove(4) - -begin_btn.click = cos_init - -[ui.append(e) for e in (grid, entity_frame, inventory_frame, stats_frame, begin_btn)] - -import random -def rcolor(): - return tuple([random.randint(0, 255) for i in range(3)]) # TODO list won't work with GridPoint.color, so had to cast to tuple - -x_max, y_max = grid.grid_size -for x in range(x_max): - for y in range(y_max): - grid.at((x,y)).color = rcolor() - -from math import pi, cos, sin -def mapgen(room_size_max = 7, room_size_min = 3, room_count = 4): - # reset map - for x in range(x_max): - for y in range(y_max): - grid.at((x, y)).walkable = False - grid.at((x, y)).transparent= False - grid.at((x,y)).tilesprite = random.choices([40, 28], weights=[.8, .2])[0] - global cos_entities - for e in cos_entities: - e.e.position = (999,999) # TODO - e.die() - cos_entities = [] - - #Dungeon generation - centers = [] - attempts = 0 - while len(centers) < room_count: - # Leaving this attempt here for later comparison. These rooms sucked. - # overlapping, uninteresting hallways, crowded into the corners sometimes, etc. - attempts += 1 - if attempts > room_count * 15: break - # room_left = random.randint(1, x_max) - # room_top = random.randint(1, y_max) - - # Take 2 - circular distribution of rooms - angle_mid = (len(centers) / room_count) * 2 * pi + 0.785 - angle = random.uniform(angle_mid - 0.25, angle_mid + 0.25) - radius = random.uniform(3, 14) - room_left = int(radius * cos(angle)) + int(x_max/2) - if room_left <= 1: room_left = 1 - if room_left > x_max - 1: room_left = x_max - 2 - room_top = int(radius * sin(angle)) + int(y_max/2) - if room_top <= 1: room_top = 1 - if room_top > y_max - 1: room_top = y_max - 2 - room_w = random.randint(room_size_min, room_size_max) - if room_w + room_left >= x_max: room_w = x_max - room_left - 2 - room_h = random.randint(room_size_min, room_size_max) - if room_h + room_top >= y_max: room_h = y_max - room_top - 2 - #print(room_left, room_top, room_left + room_w, room_top + room_h) - if any( # centers contained in this randomly generated room - [c[0] >= room_left and c[0] <= room_left + room_w and c[1] >= room_top and c[1] <= room_top + room_h for c in centers] - ): - continue # re-randomize the room position - centers.append( - (int(room_left + (room_w/2)), int(room_top + (room_h/2))) - ) - - for x in range(room_w): - for y in range(room_h): - grid.at((room_left+x, room_top+y)).walkable=True - grid.at((room_left+x, room_top+y)).transparent=True - grid.at((room_left+x, room_top+y)).tilesprite = random.choice([48, 49, 50, 51, 52, 53]) - - # generate a boulder - if (room_w > 2 and room_h > 2): - room_boulder_x, room_boulder_y = random.randint(room_left+1, room_left+room_w-1), random.randint(room_top+1, room_top+room_h-1) - cos_entities.append(BoulderEntity(room_boulder_x, room_boulder_y)) - - print(f"{room_count} rooms generated after {attempts} attempts.") - #print(centers) - # hallways - pairs = [] - for c1 in centers: - for c2 in centers: - if c1 == c2: continue - if (c2, c1) in pairs or (c1, c2) in pairs: continue - left = min(c1[0], c2[0]) - right = max(c1[0], c2[0]) - top = min(c1[1], c2[1]) - bottom = max(c1[1], c2[1]) - - corners = [(left, top), (left, bottom), (right, top), (right, bottom)] - corners.remove(c1) - corners.remove(c2) - random.shuffle(corners) - target, other = corners - for x in range(target[0], other[0], -1 if target[0] > other[0] else 1): - was_walkable = grid.at((x, target[1])).walkable - grid.at((x, target[1])).walkable=True - grid.at((x, target[1])).transparent=True - if not was_walkable: - grid.at((x, target[1])).tilesprite = random.choices([0, 12, 24], weights=[.6, .3, .1])[0] - for y in range(target[1], other[1], -1 if target[1] > other[1] else 1): - was_walkable = grid.at((target[0], y)).walkable - grid.at((target[0], y)).walkable=True - grid.at((target[0], y)).transparent=True - if not was_walkable: - grid.at((target[0], y)).tilesprite = random.choices([0, 12, 24], weights=[0.4, 0.3, 0.3])[0] - pairs.append((c1, c2)) - - - # spawn exit and button - spawn_points = [] - for x in range(x_max): - for y in range(y_max): - if grid.at((x, y)).walkable: - spawn_points.append((x, y)) - random.shuffle(spawn_points) - door_spawn, button_spawn = spawn_points[:2] - cos_entities.append(ExitEntity(*door_spawn, *button_spawn)) - - # respawn player - global player - if player: - player.position = (999,999) # TODO - die() is broken and I don't know why - player = PlayerEntity() - - - #for x in range(x_max): - # for y in range(y_max): - # if grid.at((x, y)).walkable: - # #grid.at((x,y)).tilesprite = random.choice([48, 49, 50, 51, 52, 53]) - # pass - # else: - # #grid.at((x,y)).tilesprite = random.choices([40, 28], weights=[.8, .2])[0] - -#131 - last sprite -#123, 124 - brown, grey rats -#121 - ghost -#114, 115, 116 - green, red, blue potion -#102 - shield -#98 - low armor guy, #97 - high armor guy -#89 - chest, #91 - empty chest -#84 - wizard -#82 - barrel -#66 - boulder -#64, 65 - graves -#48 - 53: ground (not going to figure out how they fit together tonight) -#42 - button-looking ground -#40 - basic solid wall -#36, 37, 38 - wall (left, middle, right) -#28 solid wall but with a grate -#21 - wide open door, 33 medium open, 45 closed door -#0 - basic dirt -class MyEntity: - def __init__(self, x=0, y=0, sprite_num=86): - self.e = mcrfpy.Entity(x, y, t, sprite_num) - grid.entities.append(self.e) - def die(self): - for i in range(len(grid.entities)): - e = grid.entities[i] - if e == self.e: - grid.entities.remove(i) - break - def bump(self, other, dx, dy): - raise NotImplementedError - - def try_move(self, dx, dy): - tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) - for e in cos_entities: - if e.e.position == (tx, ty): - #print(f"bumping {e}") - return e.bump(self, dx, dy) - if tx < 0 or tx >= x_max: - #print("out of bounds horizontally") - return False - if ty < 0 or ty >= y_max: - #print("out of bounds vertically") - return False - if grid.at((tx, ty)).walkable == True: - #print("Motion!") - self.e.position = (tx, ty) - return True - else: - #print("Bonk") - return False - - def _relative_move(self, dx, dy): - tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) - self.e.position = (tx, ty) - - - def move(self, direction): - if direction == "N": - self.try_move(0, -1) - elif direction == "E": - self.try_move(1, 0) - elif direction == "S": - self.try_move(0, 1) - elif direction == "W": - self.try_move(-1, 0) - -cos_entities = [] - -class PlayerEntity(MyEntity): - def __init__(self): - # find spawn point - spawn_points = [] - for x in range(x_max): - for y in range(y_max): - if grid.at((x, y)).walkable: - spawn_points.append((x, y)) - random.shuffle(spawn_points) - for spawn in spawn_points: - for e in cos_entities: - if e.e.position == spawn: break - else: - break - - #print(f"spawn at {spawn}") - super().__init__(spawn[0], spawn[1], sprite_num=84) - -class BoulderEntity(MyEntity): - def __init__(self, x, y): - super().__init__(x, y, 66) - - def bump(self, other, dx, dy): - if type(other) == BoulderEntity: - #print("Boulders can't push boulders") - return False - tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) - # Is the boulder blocked the same direction as the bumper? If not, let's both move - old_pos = int(self.e.position[0]), int(self.e.position[1]) - if self.try_move(dx, dy): - other.e.position = old_pos - return True - -class ButtonEntity(MyEntity): - def __init__(self, x, y, exit): - super().__init__(x, y, 42) - self.exit = exit - - def bump(self, other, dx, dy): - if type(other) == BoulderEntity: - self.exit.unlock() - other._relative_move(dx, dy) - return True - -class ExitEntity(MyEntity): - def __init__(self, x, y, bx, by): - super().__init__(x, y, 45) - self.my_button = ButtonEntity(bx, by, self) - self.unlocked = False - global cos_entities - cos_entities.append(self.my_button) - - def unlock(self): - self.e.sprite_number = 21 - self.unlocked = True - - def lock(self): - self.e.sprite_number = 45 - self.unlocked = True - - def bump(self, other, dx, dy): - if type(other) == BoulderEntity: - return False - if self.unlocked: - other._relative_move(dx, dy) - -player = None diff --git a/src/scripts/cos_tiles.py b/src/scripts/cos_tiles.py new file mode 100644 index 0000000..d7cb57b --- /dev/null +++ b/src/scripts/cos_tiles.py @@ -0,0 +1,223 @@ +tiles = {} +deltas = [ + (-1, -1), ( 0, -1), (+1, -1), + (-1, 0), ( 0, 0), (+1, 0), + (-1, +1), ( 0, +1), (+1, +1) + ] + +class TileInfo: + GROUND, WALL, DONTCARE = True, False, None + chars = { + "X": WALL, + "_": GROUND, + "?": DONTCARE + } + symbols = {v: k for k, v in chars.items()} + + def __init__(self, values:dict): + self._values = values + self.rules = [] + self.chance = 1.0 + + @staticmethod + def from_grid(grid, xy:tuple): + values = {} + for d in deltas: + tx, ty = d[0] + xy[0], d[1] + xy[1] + try: + values[d] = grid.at((tx, ty)).walkable + except ValueError: + values[d] = True + return TileInfo(values) + + @staticmethod + def from_string(s): + values = {} + for d, c in zip(deltas, s): + values[d] = TileInfo.chars[c] + return TileInfo(values) + + def __hash__(self): + """for use as a dictionary key""" + return hash(tuple(self._values.items())) + + def match(self, other:"TileInfo"): + for d, rule in self._values.items(): + if rule is TileInfo.DONTCARE: continue + if other._values[d] is TileInfo.DONTCARE: continue + if rule != other._values[d]: return False + return True + + def show(self): + nine = ['', '', '\n'] * 3 + for k, end in zip(deltas, nine): + c = TileInfo.symbols[self._values[k]] + print(c, end=end) + + def __repr__(self): + return f"" + +cardinal_directions = { + "N": ( 0, -1), + "S": ( 0, +1), + "E": (-1, 0), + "W": (+1, 0) +} + +def special_rule_verify(rule, grid, xy, unverified_tiles, pass_unverified=False): + cardinal, allowed_tile = rule + dxy = cardinal_directions[cardinal.upper()] + tx, ty = xy[0] + dxy[0], xy[1] + dxy[1] + #print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}") + if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified + try: + return grid.at((tx, ty)).tilesprite == allowed_tile + except ValueError: + return False + +import random +tile_of_last_resort = 431 + +def find_possible_tiles(grid, x, y, unverified_tiles=None, pass_unverified=False): + ti = TileInfo.from_grid(grid, (x, y)) + if unverified_tiles is None: unverified_tiles = [] + matches = [(k, v) for k, v in tiles.items() if k.match(ti)] + if not matches: + return [] + possible = [] + if not any([tileinfo.rules for tileinfo, _ in matches]): + # make weighted choice, as the tile does not depend on verification + wts = [k.chance for k, v in matches] + tileinfo, tile = random.choices(matches, weights=wts)[0] + return [tile] + + for tileinfo, tile in matches: + if not tileinfo.rules: + possible.append(tile) + continue + for r in tileinfo.rules: #for r in ...: if ... continue == more readable than an "any" 1-liner + p = special_rule_verify(r, grid, (x,y), + unverified_tiles=unverified_tiles, + pass_unverified = pass_unverified + ) + if p: + possible.append(tile) + continue + return list(set(list(possible))) + +def wfc_first_pass(grid): + w, h = grid.grid_size + possibilities = {} + for x in range(0, w): + for y in range(0, h): + matches = find_possible_tiles(grid, x, y, pass_unverified=True) + if len(matches) == 0: + grid.at((x, y)).tilesprite = tile_of_last_resort + possibilities[(x,y)] = matches + elif len(matches) == 1: + grid.at((x, y)).tilesprite = matches[0] + else: + possibilities[(x,y)] = matches + return possibilities + +def wfc_pass(grid, possibilities=None): + w, h = grid.grid_size + if possibilities is None: + #print("first pass results:") + possibilities = wfc_first_pass(grid) + counts = {} + for v in possibilities.values(): + if len(v) in counts: counts[len(v)] += 1 + else: counts[len(v)] = 1 + print(counts) + return possibilities + elif len(possibilities) == 0: + print("We're done!") + return + old_possibilities = possibilities + possibilities = {} + for (x, y) in old_possibilities.keys(): + matches = find_possible_tiles(grid, x, y, unverified_tiles=old_possibilities.keys(), pass_unverified = True) + if len(matches) == 0: + print((x,y), matches) + grid.at((x, y)).tilesprite = tile_of_last_resort + possibilities[(x,y)] = matches + elif len(matches) == 1: + grid.at((x, y)).tilesprite = matches[0] + else: + grid.at((x, y)).tilesprite = -1 + grid.at((x, y)).color = (32 * len(matches), 32 * len(matches), 32 * len(matches)) + possibilities[(x,y)] = matches + + if len(possibilities) == len(old_possibilities): + #print("No more tiles could be solved without collapse") + counts = {} + for v in possibilities.values(): + if len(v) in counts: counts[len(v)] += 1 + else: counts[len(v)] = 1 + #print(counts) + if 0 in counts: del counts[0] + if len(counts) == 0: + print("Contrats! You broke it! (insufficient tile defs to solve remaining tiles)") + return [] + target = min(list(counts.keys())) + while possibilities: + for (x, y) in possibilities.keys(): + if len(possibilities[(x, y)]) != target: + continue + ti = TileInfo.from_grid(grid, (x, y)) + matches = [(k, v) for k, v in tiles.items() if k.match(ti)] + verifiable_matches = find_possible_tiles(grid, x, y, unverified_tiles=possibilities.keys()) + if not verifiable_matches: continue + #print(f"collapsing {(x, y)} ({target} choices)") + matches = [(k, v) for k, v in matches if v in verifiable_matches] + wts = [k.chance for k, v in matches] + tileinfo, tile = random.choices(matches, weights=wts)[0] + grid.at((x, y)).tilesprite = tile + del possibilities[(x, y)] + break + else: + selected = random.choice(list(possibilities.keys())) + #print(f"No tiles have verifable solutions: QUANTUM -> {selected}") + # sprinkle some quantumness on it + ti = TileInfo.from_grid(grid, (x, y)) + matches = [(k, v) for k, v in tiles.items() if k.match(ti)] + wts = [k.chance for k, v in matches] + if not wts: + print(f"This one: {(x,y)} {matches}\n{wts}") + del possibilities[(x, y)] + return possibilities + tileinfo, tile = random.choices(matches, weights=wts)[0] + grid.at((x, y)).tilesprite = tile + del possibilities[(x, y)] + + return possibilities + +#with open("scripts/tile_def.txt", "r") as f: +with open("scripts/simple_tiles.txt", "r") as f: + for block in f.read().split('\n\n'): + info, constraints = block.split('\n', 1) + if '#' in info: + info, comment = info.split('#', 1) + rules = [] + if '@' in info: + info, *block_rules = info.split('@') + #print(block_rules) + for r in block_rules: + rules.append((r[0], int(r[1:]))) + #cardinal_dir = block_rules[0] + #partner + if ':' not in info: + tile_id = int(info) + chance = 1.0 + else: + tile_id, chance = info.split(':') + tile_id = int(tile_id) + chance = float(chance.strip()) + constraints = constraints.replace('\n', '') + k = TileInfo.from_string(constraints) + k.rules = rules + k.chance = chance + tiles[k] = tile_id + + diff --git a/src/scripts/game.py b/src/scripts/game.py index 962fc91..b6ce058 100644 --- a/src/scripts/game.py +++ b/src/scripts/game.py @@ -1,60 +1,109 @@ import mcrfpy +#t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) # 12, 11) +#t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) # 12, 11) font = mcrfpy.Font("assets/JetbrainsMono.ttf") -texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) -print("[game.py] Default texture:") -print(mcrfpy.default_texture) -print(type(mcrfpy.default_texture)) +frame_color = (64, 64, 128) -# build test widgets +import random +import cos_entities as ce +import cos_level as cl +#import cos_tiles as ct -mcrfpy.createScene("pytest") -mcrfpy.setScene("pytest") -ui = mcrfpy.sceneUI("pytest") +class Crypt: + def __init__(self): + mcrfpy.createScene("play") + self.ui = mcrfpy.sceneUI("play") + mcrfpy.setScene("play") + mcrfpy.keypressScene(self.cos_keys) -# Frame -f = mcrfpy.Frame(25, 19, 462, 346, fill_color=(255, 92, 92)) -print("Frame alive") -# fill (LinkedColor / Color): f.fill_color -# outline (LinkedColor / Color): f.outline_color -# pos (LinkedVector / Vector): f.pos -# size (LinkedVector / Vector): f.size + entity_frame = mcrfpy.Frame(815, 10, 194, 595, fill_color = frame_color) + inventory_frame = mcrfpy.Frame(10, 610, 800, 143, fill_color = frame_color) + stats_frame = mcrfpy.Frame(815, 610, 194, 143, fill_color = frame_color) -# Caption -print("Caption attempt w/ fill_color:") -#c = mcrfpy.Caption(512+25, 19, "Hi.", font) -#c = mcrfpy.Caption(512+25, 19, "Hi.", font, fill_color=(255, 128, 128)) -c = mcrfpy.Caption(512+25, 19, "Hi.", font, fill_color=mcrfpy.Color(255, 128, 128), outline_color=(128, 255, 128)) -print("Caption alive") -# fill (LinkedColor / Color): c.fill_color -#color_val = c.fill_color -print(c.fill_color) -#print("Set a fill color") -#c.fill_color = (255, 255, 255) -print("Lol, did it segfault?") -# outline (LinkedColor / Color): c.outline_color -# font (Font): c.font -# pos (LinkedVector / Vector): c.pos + #self.level = cl.Level(30, 23) + self.entities = [] + self.depth=1 + self.create_level(self.depth) + #self.grid = mcrfpy.Grid(20, 15, t, (10, 10), (1014, 758)) + self.player = ce.PlayerEntity(game=self) + self.swap_level(self.level, self.spawn_point) -# Sprite -s = mcrfpy.Sprite(25, 384+19, texture, 86, 9.0) -# pos (LinkedVector / Vector): s.pos -# texture (Texture): s.texture + # Test Entities + #ce.BoulderEntity(9, 7, game=self) + #ce.BoulderEntity(9, 8, game=self) + #ce.ExitEntity(12, 6, 14, 4, game=self) + # scene setup -# Grid -g = mcrfpy.Grid(10, 10, texture, 512+25, 384+19, 462, 346) -# texture (Texture): g.texture -# pos (LinkedVector / Vector): g.pos -# size (LinkedVector / Vector): g.size -for _x in range(10): - for _y in range(10): - g.at((_x, _y)).color = (255 - _x*25, 255 - _y*25, 255) -g.zoom = 2.0 + [self.ui.append(e) for e in (self.grid,)] # entity_frame, inventory_frame, stats_frame)] -[ui.append(d) for d in (f, c, s, g)] + self.possibilities = None # track WFC possibilities between rounds -print("built!") + def add_entity(self, e:ce.COSEntity): + self.entities.append(e) + self.entities.sort(key = lambda e: e.draw_order, reverse=False) + # hack / workaround for grid.entities not interable + while len(self.grid.entities): # while there are entities on the grid, + self.grid.entities.remove(0) # remove the 1st ("0th") + for e in self.entities: + self.grid.entities.append(e._entity) -# tests + def create_level(self, depth): + #if depth < 3: + # features = None + self.level = cl.Level(30, 23) + self.grid = self.level.grid + coords = self.level.generate() + self.entities = [] + for k, v in coords.items(): + if k == "spawn": + self.spawn_point = v + elif k == "boulder": + ce.BoulderEntity(v[0], v[1], game=self) + elif k == "button": + pass + elif k == "exit": + ce.ExitEntity(v[0], v[1], coords["button"][0], coords["button"][1], game=self) + def cos_keys(self, key, state): + d = None + if state == "end": return + elif key == "W": d = (0, -1) + elif key == "A": d = (-1, 0) + elif key == "S": d = (0, 1) + elif key == "D": d = (1, 0) + elif key == "M": self.level.generate() + elif key == "R": + self.level.reset() + self.possibilities = None + elif key == "T": + self.level.split() + self.possibilities = None + elif key == "Y": self.level.split(single=True) + elif key == "U": self.level.highlight(+1) + elif key == "I": self.level.highlight(-1) + elif key == "O": + self.level.wall_rooms() + self.possibilities = None + #elif key == "P": ct.format_tiles(self.grid) + elif key == "P": + self.possibilities = ct.wfc_pass(self.grid, self.possibilities) + if d: self.player.try_move(*d) + + def swap_level(self, new_level, spawn_point): + self.level = new_level + self.grid = self.level.grid + self.grid.zoom = 2.0 + # TODO, make an entity mover function + self.add_entity(self.player) + self.player.grid = self.grid + self.player.draw_pos = spawn_point + self.grid.entities.append(self.player._entity) + try: + self.ui.remove(0) + except: + pass + self.ui.append(self.grid) + +crypt = Crypt() diff --git a/src/scripts/game_old.py b/src/scripts/game_old.py deleted file mode 100644 index 2975588..0000000 --- a/src/scripts/game_old.py +++ /dev/null @@ -1,221 +0,0 @@ -#print("Hello mcrogueface") -import mcrfpy -import cos_play -# Universal stuff -font = mcrfpy.Font("assets/JetbrainsMono.ttf") -texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) #12, 11) -texture_cold = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) #12, 11) -texture_hot = mcrfpy.Texture("assets/kenney_lava.png", 16, 16) #12, 11) - -# Test stuff -mcrfpy.createScene("boom") -mcrfpy.setScene("boom") -ui = mcrfpy.sceneUI("boom") -box = mcrfpy.Frame(40, 60, 200, 300, fill_color=(255,128,0), outline=4.0, outline_color=(64,64,255,96)) -ui.append(box) - -#caption = mcrfpy.Caption(10, 10, "Clicky", font, (255, 255, 255, 255), (0, 0, 0, 255)) -#box.click = lambda x, y, btn, type: print("Hello callback: ", x, y, btn, type) -#box.children.append(caption) - -test_sprite_number = 86 -sprite = mcrfpy.Sprite(20, 60, texture, test_sprite_number, 4.0) -spritecap = mcrfpy.Caption(5, 5, "60", font) -def click_sprite(x, y, btn, action): - global test_sprite_number - if action != "start": return - if btn in ("left", "wheel_up"): - test_sprite_number -= 1 - elif btn in ("right", "wheel_down"): - test_sprite_number += 1 - sprite.sprite_number = test_sprite_number # TODO - inconsistent naming for __init__, __repr__ and getsetter: sprite_number vs sprite_index - spritecap.text = test_sprite_number - -sprite.click = click_sprite # TODO - sprites don't seem to correct for screen position or scale when clicking -box.children.append(sprite) -box.children.append(spritecap) -box.click = click_sprite - -f_a = mcrfpy.Frame(250, 60, 80, 80, fill_color=(255, 92, 92)) -f_a_txt = mcrfpy.Caption(5, 5, "0", font) - -f_b = mcrfpy.Frame(340, 60, 80, 80, fill_color=(92, 255, 92)) -f_b_txt = mcrfpy.Caption(5, 5, "0", font) - -f_c = mcrfpy.Frame(430, 60, 80, 80, fill_color=(92, 92, 255)) -f_c_txt = mcrfpy.Caption(5, 5, "0", font) - - -ui.append(f_a) -f_a.children.append(f_a_txt) -ui.append(f_b) -f_b.children.append(f_b_txt) -ui.append(f_c) -f_c.children.append(f_c_txt) - -import sys -def ding(*args): - f_a_txt.text = str(sys.getrefcount(ding)) + " refs" - f_b_txt.text = sys.getrefcount(dong) - f_c_txt.text = sys.getrefcount(stress_test) - -def dong(*args): - f_a_txt.text = str(sys.getrefcount(ding)) + " refs" - f_b_txt.text = sys.getrefcount(dong) - f_c_txt.text = sys.getrefcount(stress_test) - -running = False -timers = [] - -def add_ding(): - global timers - n = len(timers) - mcrfpy.setTimer(f"timer{n}", ding, 100) - print("+1 ding:", timers) - -def add_dong(): - global timers - n = len(timers) - mcrfpy.setTimer(f"timer{n}", dong, 100) - print("+1 dong:", timers) - -def remove_random(): - global timers - target = random.choice(timers) - print("-1 timer:", target) - print("remove from list") - timers.remove(target) - print("delTimer") - mcrfpy.delTimer(target) - print("done") - -import random -import time -def stress_test(*args): - global running - global timers - if not running: - print("stress test initial") - running = True - timers.append("recurse") - add_ding() - add_dong() - mcrfpy.setTimer("recurse", stress_test, 1000) - mcrfpy.setTimer("terminate", lambda *args: mcrfpy.delTimer("recurse"), 30000) - ding(); dong() - else: - #print("stress test random activity") - #random.choice([ - # add_ding, - # add_dong, - # remove_random - # ])() - #print(timers) - print("Segfaultin' time") - mcrfpy.delTimer("recurse") - print("Does this still work?") - time.sleep(0.5) - print("How about now?") - - -stress_test() - - -# Loading Screen -mcrfpy.createScene("loading") -ui = mcrfpy.sceneUI("loading") -#mcrfpy.setScene("loading") -logo_texture = mcrfpy.Texture("assets/temp_logo.png", 1024, 1024)#1, 1) -logo_sprite = mcrfpy.Sprite(50, 50, logo_texture, 0, 0.5) -ui.append(logo_sprite) -logo_sprite.click = lambda *args: mcrfpy.setScene("menu") -logo_caption = mcrfpy.Caption(70, 600, "Click to Proceed", font, (255, 0, 0, 255), (0, 0, 0, 255)) -#logo_caption.fill_color =(255, 0, 0, 255) -ui.append(logo_caption) - - -# menu screen -mcrfpy.createScene("menu") - -for e in [ - mcrfpy.Caption(10, 10, "Crypt of Sokoban", font, (255, 255, 255), (0, 0, 0)), - mcrfpy.Caption(20, 55, "a McRogueFace demo project", font, (192, 192, 192), (0, 0, 0)), - mcrfpy.Frame(15, 70, 150, 60, fill_color=(64, 64, 128)), - mcrfpy.Frame(15, 145, 150, 60, fill_color=(64, 64, 128)), - mcrfpy.Frame(15, 220, 150, 60, fill_color=(64, 64, 128)), - mcrfpy.Frame(15, 295, 150, 60, fill_color=(64, 64, 128)), - #mcrfpy.Frame(900, 10, 100, 100, fill_color=(255, 0, 0)), - ]: - mcrfpy.sceneUI("menu").append(e) - -def click_once(fn): - def wraps(*args, **kwargs): - #print(args) - action = args[3] - if action != "start": return - return fn(*args, **kwargs) - return wraps - -@click_once -def asdf(x, y, btn, action): - print(f"clicky @({x},{y}) {action}->{btn}") - -@click_once -def clicked_exit(*args): - mcrfpy.exit() - -menu_btns = [ - ("Boom", lambda *args: 1 / 0), - ("Exit", clicked_exit), - ("About", lambda *args: mcrfpy.setScene("about")), - ("Settings", lambda *args: mcrfpy.setScene("settings")), - ("Start", lambda *args: mcrfpy.setScene("play")) - ] -for i in range(len(mcrfpy.sceneUI("menu"))): - e = mcrfpy.sceneUI("menu")[i] # TODO - fix iterator - #print(e, type(e)) - if type(e) is not mcrfpy.Frame: continue - label, fn = menu_btns.pop() - #print(label) - e.children.append(mcrfpy.Caption(5, 5, label, font, (192, 192, 255), (0,0,0))) - e.click = fn - - -# settings screen -mcrfpy.createScene("settings") -window_scaling = 1.0 - -scale_caption = mcrfpy.Caption(180, 70, "1.0x", font, (255, 255, 255), (0, 0, 0)) -#scale_caption.fill_color = (255, 255, 255) # TODO - mcrfpy.Caption.__init__ is not setting colors -for e in [ - mcrfpy.Caption(10, 10, "Settings", font, (255, 255, 255), (0, 0, 0)), - mcrfpy.Frame(15, 70, 150, 60, fill_color=(64, 64, 128)), # + - mcrfpy.Frame(300, 70, 150, 60, fill_color=(64, 64, 128)), # - - mcrfpy.Frame(15, 295, 150, 60, fill_color=(64, 64, 128)), - scale_caption, - ]: - mcrfpy.sceneUI("settings").append(e) - -@click_once -def game_scale(x, y, btn, action, delta): - global window_scaling - print(f"WIP - scale the window from {window_scaling:.1f} to {window_scaling+delta:.1f}") - window_scaling += delta - scale_caption.text = f"{window_scaling:.1f}x" - mcrfpy.setScale(window_scaling) - #mcrfpy.setScale(2) - -settings_btns = [ - ("back", lambda *args: mcrfpy.setScene("menu")), - ("-", lambda x, y, btn, action: game_scale(x, y, btn, action, -0.1)), - ("+", lambda x, y, btn, action: game_scale(x, y, btn, action, +0.1)) - ] - -for i in range(len(mcrfpy.sceneUI("settings"))): - e = mcrfpy.sceneUI("settings")[i] # TODO - fix iterator - #print(e, type(e)) - if type(e) is not mcrfpy.Frame: continue - label, fn = settings_btns.pop() - #print(label, fn) - e.children.append(mcrfpy.Caption(5, 5, label, font, (192, 192, 255), (0,0,0))) - e.click = fn