diff --git a/lua/neohack/actions.lua b/lua/neohack/actions.lua index 6b5902f..3abd3ae 100644 --- a/lua/neohack/actions.lua +++ b/lua/neohack/actions.lua @@ -1,4 +1,5 @@ local chat = require("neohack.chat") +local inventory = require("neohack.inventory") local M = { leader_key = nil, tick = nil, @@ -13,7 +14,7 @@ local utils = require("neohack.utils") M.actions = { ---comment inventory = function() - message.notify("Level " .. state.current_floor .. " " .. player.show_inventory()) + message.notify("You have " .. player.get_inventory()) end, ---comment @@ -61,7 +62,7 @@ M.actions = { wait = function(request) local count = utils.string_to_int(request.object) if count then - message.notify("Waiting " .. count .. " turns") + message.notify(utils.capitalize_first_letter(request.action) .. "ing " .. count .. " turns") for _ = 1, count do M.tick() end @@ -87,6 +88,42 @@ M.actions = { end, } +M.synonyms = { + h = "help", + ["?"] = "help", + ["-h"] = "help", + ["-help"] = "help", + ["--help"] = "help", + + i = "inventory", + items = "inventory", + + k = "kick", + + w = "wear", + equip = "wear", + wield = "wear", + + f = "fuse", + + l = "look", + read = "look", + examine = "look", + + e = "eat", + + d = "drop", + + s = "say", + speak = "say", + yell = "say", + intone = "say", + + sleep = "wait", + sit = "wait", + rest = "wait", +} + ---Insert the action command ---@param action_string string M.insert_action = function(action_string) @@ -104,7 +141,7 @@ end ---comment M.prompt_wear = function() - message.notify("You have:\n0:nothing\n" .. player.get_inventory_item_with_index()) + message.notify("You have:\n0:nothing\n" .. inventory.get_inventory_item_with_index()) vim.defer_fn(function() local object = M.prompt_one_word("Wear what?") if object then @@ -119,7 +156,7 @@ end ---comment M.prompt_fuse = function() - message.notify("You have:\n" .. player.get_inventory_item_with_index()) + message.notify("You have:\n" .. inventory.get_inventory_item_with_index()) vim.defer_fn(function() local index = M.prompt("Fuse what?") if index then @@ -134,7 +171,7 @@ end ---comment M.prompt_look = function() - message.notify("You have:\n0:self\n" .. player.get_inventory_item_with_index()) + message.notify("You have:\n0:self\n" .. inventory.get_inventory_item_with_index()) vim.defer_fn(function() local index = M.prompt_one_word("Look at what?") if index then @@ -146,7 +183,7 @@ end ---comment M.prompt_eat = function() - message.notify("You have:\n" .. player.get_inventory_item_with_index()) + message.notify("You have:\n" .. inventory.get_inventory_item_with_index()) vim.defer_fn(function() local index = M.prompt_one_word("Eat what?") if index then @@ -158,7 +195,7 @@ end ---comment M.prompt_drop = function() - message.notify("You have:\n" .. player.get_inventory_item_with_index()) + message.notify("You have:\n" .. inventory.get_inventory_item_with_index()) vim.defer_fn(function() local index = M.prompt_one_word("Drop what?") if index then @@ -193,17 +230,29 @@ M.prompt_wait = function() end, 50) end ----parse a string into performing an action +---parse a string into an action and the request ---@param inserted_chars string +---@return function? +---@return Request? M.parse_action = function(inserted_chars) local request = chat.parse_request(inserted_chars) if request == nil then - return + return nil, nil end - local action = M.actions[request.action] + local action_word = M.synonyms[request.action] or request.action + local action = M.actions[action_word] if action == nil then - chat.no_action(request) + return chat.no_action, request else + return action, request + end +end + +---perform an action from free text +---@param inserted_chars string +M.execute_action = function(inserted_chars) + local action, request = M.parse_action(inserted_chars) + if action then action(request) end end diff --git a/lua/neohack/buffer.lua b/lua/neohack/buffer.lua index 54112a8..7895502 100644 --- a/lua/neohack/buffer.lua +++ b/lua/neohack/buffer.lua @@ -1,6 +1,8 @@ --- all the interactions with the buffer that backs the map --- +local view_buffer = require("neohack.view_buffer") + ---@class Buffer --- @field bufnr integer --- @field level integer @@ -19,16 +21,11 @@ local M = { --- level to bufnr ---@type table levels = {}, - AutoCmdGroup = "NeoHackGameTick", } -local defs = require("neohack.defs") local state = require("neohack.state") local message = require("neohack.message") -local namespace = vim.api.nvim_create_namespace("NeoHack") -local group = vim.api.nvim_create_augroup(M.AutoCmdGroup, { clear = true }) - --- Initialize buffer-specific data local function init_buffer(bufnr, handlers) return { @@ -45,7 +42,7 @@ local function init_buffer(bufnr, handlers) end M.create_new_buffer = function(lines, handlers) - local bufnr = lines and M.new_buffer(lines) or M.copy_buffer() + local bufnr = lines and view_buffer.new_buffer(lines) or view_buffer.copy_buffer() M.buffers[bufnr] = init_buffer(bufnr, handlers) table.insert(M.levels, bufnr) M.buffers[bufnr].level = #M.levels @@ -53,31 +50,18 @@ M.create_new_buffer = function(lines, handlers) return bufnr, #M.levels end ---- Copy the current buffer and return the new buffer number ----@return integer bufnr -M.copy_buffer = function() - local current_bufnr = vim.api.nvim_get_current_buf() - local lines = vim.api.nvim_buf_get_lines(current_bufnr, 0, -1, false) - return M.new_buffer(lines) +M.setup_buffer = function(bufnr, first_floor) + view_buffer.setup_buffer(bufnr, first_floor) + M.add_handlers(bufnr) + M.setup_game_buffer(bufnr) end ----comment ----@param lines string[] ----@return integer -M.new_buffer = function(lines) - local new_bufnr = vim.api.nvim_create_buf(true, false) - vim.api.nvim_buf_set_lines(new_bufnr, 0, -1, false, lines) - vim.api.nvim_set_current_buf(new_bufnr) - return new_bufnr +M.setup_game_buffer = function(bufnr) + view_buffer.setup_game_buffer(bufnr, M.buffers[bufnr]) end -M.setup_buffer = function(bufnr, first_floor) - vim.api.nvim_set_current_buf(bufnr) - if first_floor then - M.set_cursor(first_floor.row, first_floor.col) - end - M.add_handlers(bufnr) - M.setup_game_buffer(bufnr) +M.restore_original_settings = function(bufnr) + view_buffer.restore_original_settings(bufnr, M.buffers[bufnr]) end M.end_game = function() @@ -87,7 +71,7 @@ M.end_game = function() -- M.handle_insert = nil -- M.handle_changed = nil -- M.handle_yanked = nil - M.remove_handlers(bufnr) + view_buffer.remove_handlers(bufnr) M.restore_original_settings(bufnr) end end @@ -103,139 +87,22 @@ M.add_handlers = function(bufnr) and buf_data.handle_changed and buf_data.handle_yanked then - M.add_moved_handlers(bufnr) - M.add_insert_handlers(bufnr) - M.add_change_handlers(bufnr) - M.add_yank_handlers(bufnr) + view_buffer.add_handler(buf_data, "CursorMoved", buf_data.handle_moved) + view_buffer.add_handler(buf_data, "TextChangedI", buf_data.handle_insert) + view_buffer.add_handler(buf_data, "InsertEnter", buf_data.handle_insert_enter) + -- changed is part of CursorMoved now + -- view_buffer.add_handler(buf_data, "TextChanged", buf_data.handle_changed) + view_buffer.add_handler(buf_data, "TextYankPost", buf_data.handle_yanked) else error("missing handlers") end end ---- Remove handlers for a specific buffer ----@param bufnr integer -M.remove_handlers = function(bufnr) - vim.api.nvim_clear_autocmds({ group = M.AutoCmdGroup, buffer = bufnr }) -end - ---- Add moved handlers for a specific buffer ----@param bufnr integer -M.add_moved_handlers = function(bufnr) - vim.api.nvim_create_autocmd("CursorMoved", { - group = group, - buffer = bufnr, - callback = function() - -- M.remove_handlers(bufnr) - M.buffers[bufnr].handle_moved() - -- M.add_handlers(bufnr) - end, - }) -end - ---- Add insert handlers for a specific buffer ----@param bufnr integer -M.add_insert_handlers = function(bufnr) - -- vim.api.nvim_create_autocmd("InsertCharPre", { - vim.api.nvim_create_autocmd("TextChangedI", { - group = group, - buffer = bufnr, - callback = function() - M.buffers[bufnr].handle_insert() - -- vim.schedule(function() - -- M.write_buf(bufnr) - -- end) - end, - }) - - vim.api.nvim_create_autocmd("InsertEnter", { - group = group, - buffer = bufnr, - callback = function() - M.buffers[bufnr].handle_insert_enter() - -- vim.schedule(function() - -- M.write_buf(bufnr) - -- end) - end, - }) -end - ----TODO: deprecated cursor moved also handles changed ---- Add change handlers for a specific buffer ----@param bufnr integer -M.add_change_handlers = function(bufnr) - vim.api.nvim_create_autocmd("TextChanged", { - group = group, - buffer = bufnr, - callback = function() - -- M.remove_handlers(bufnr) - ---TODO: deprecated cursor moved also handles changed - -- M.buffers[bufnr].handle_changed() - -- M.add_handlers(bufnr) - end, - }) -end - ---- Add yank handlers for a specific buffer ----@param bufnr integer -M.add_yank_handlers = function(bufnr) - vim.api.nvim_create_autocmd("TextYankPost", { - group = group, - buffer = bufnr, - callback = function() - -- M.remove_handlers(bufnr) - M.buffers[bufnr].handle_yanked() - -- M.add_handlers(bufnr) - end, - }) -end - ---- Apply the callback to move to the next frame for a specific buffer ----@param callback function() -M.tick = function(callback) - callback() - M.write_buf(nil) -end - ---TODO: is this actually used now that maps are generated? it creates entities from the existing chars ---- Read buffer content and update the cells for a specific buffer ----@param bufnr integer ----@return Entity[][] -M.read_a_buf = function(bufnr) - local new_cells = {} -- prepare a new frame - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - for row, value in ipairs(lines) do - local result = {} - for col = 1, #value do - local char = value:sub(col, col) - result[col] = defs.new_entity_from_char(char, row, col) - end - new_cells[row] = result - end - return new_cells -end - ---- Read buffer content as a grid of chars ----@param bufnr integer ----@return string[][] -M.read_buf_chars = function(bufnr) - local new_cells = {} -- prepare a new frame - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - for row, value in ipairs(lines) do - local result = {} - for col = 1, #value do - local char = value:sub(col, col) - result[col] = char - end - new_cells[row] = result - end - return new_cells -end - --- Read buffer content and update the cells for a specific buffer ---@param bufnr integer M.read_buf = function(bufnr) -- replace the old frame - M.buffers[bufnr].cells = M.read_a_buf(bufnr) + M.buffers[bufnr].cells = view_buffer.read_a_buf(bufnr) end --- Write the buffer content based on the cells for a specific buffer @@ -248,32 +115,27 @@ M.write_buf = function(bufnr) error("no bufnr") end end + view_buffer.write_buf(bufnr, M.buffers[bufnr].cells) +end - local lines = {} - for row_index, row in ipairs(M.buffers[bufnr].cells) do - local value = {} - for _, col in ipairs(row) do - if col.visible then - table.insert(value, col.char) - elseif col.seen then - table.insert(value, col.char) - else - table.insert(value, defs.not_visible) - end - end - lines[row_index] = table.concat(value) - end - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) +--- Apply the callback to move to the next frame for a specific buffer +---@param callback function() +M.tick = function(callback) + callback() + M.write_buf(nil) end --- Get the entity at a specific position in a specific buffer ---@param row integer ---@param col integer ----@return Entity? +---@return Entity M.get_entity_at_pos = function(row, col) local bufnr = state.current_bufnr local line = M.buffers[bufnr].cells[row] if not line then + -- TODO: is this an actual error? + -- message.notify("no line at " .. row) + ---@diagnostic disable-next-line: return-type-mismatch return nil end local entity = line[col] @@ -294,10 +156,9 @@ end ---@return integer -- col 1 indexed ---@return Entity M.get_under_cursor = function() - local bufnr = state.current_bufnr local row, col = unpack(vim.api.nvim_win_get_cursor(0)) col = col + 1 - local entity = M.buffers[bufnr].cells[row][col] + local entity = M.get_entity_at_pos(row, col) -- message.notify("under cursor " .. row .. " " .. col .. " " .. entity.char) return row, col, entity end @@ -328,22 +189,6 @@ M.insert_entity_at_cell = function(row, col, entity) end end ---- Set highlight for a specific position in a specific buffer ----@param row integer ----@param col integer ----@param highlight string? -M.set_highlight = function(row, col, highlight) - local bufnr = state.current_bufnr - if not bufnr then - error("no bufnr") - end - if highlight then - vim.defer_fn(function() - vim.api.nvim_buf_add_highlight(bufnr, namespace, highlight, row - 1, col - 1, col) - end, 1) - end -end - --- Highlight cursor line and column with specific settings ---@param highlight table M.highlight_cursor = function(highlight) @@ -357,103 +202,4 @@ M.highlight_cursor = function(highlight) end, 50) end ---- Highlight hit at a specific position in a specific buffer ----@param row integer ----@param col integer ----@param highlight string ----@param timeout integer -M.highlight_hit = function(row, col, highlight, timeout) - local bufnr = state.current_bufnr - if not bufnr then - error("no bufnr") - end - M.set_highlight(row, col, highlight) - vim.defer_fn(function() - vim.api.nvim_buf_clear_namespace(bufnr, namespace, row - 1, row) - end, timeout) -end - ----comment ----@param row integer ----@param col integer -M.set_cursor = function(row, col) - vim.api.nvim_win_set_cursor(0, { row, col - 1 }) -end - ----comment -M.move_prev_cursor = function() - -- TODO: show the bouncing effect using smear-cursor - -- vim.defer_fn(function() - M.set_cursor(state.prev_cursor.row, state.prev_cursor.col) - -- end, 10) -end - --- TODO: do we need to save/restore buffer settings anymore with a copy of the buffer? ----comment ----@return integer bufnr -M.setup_game_buffer = function(bufnr) - local winnr = 0 - if not bufnr then - error("No buffer found") - end - M.buffers[bufnr].buf_settings = { - spell = vim.wo[winnr].spell, - hlsearch = vim.api.nvim_get_option("hlsearch"), - wrap = vim.wo[winnr].wrap, - listchars = vim.o.listchars, - cursorline_hl = vim.api.nvim_get_hl(0, { name = "CursorLine", link = false }), - cursorcolumn_hl = vim.api.nvim_get_hl(0, { name = "CursorColumn", link = false }), - statusline = vim.o.statusline, - laststatus = vim.o.laststatus, - } - - vim.api.nvim_buf_clear_namespace(bufnr, -1, 0, -1) - - vim.api.nvim_buf_set_option(bufnr, "cursorline", true) - vim.api.nvim_buf_set_option(bufnr, "cursorcolumn", true) - - vim.api.nvim_buf_set_option(bufnr, "spell", false) - vim.cmd.nohlsearch() - vim.cmd("set nowrap") - vim.o.listchars = "" - - -- set the statusline to show in game info - vim.o.statusline = "%!v:lua.require'neohack'.status_line()" - vim.o.laststatus = 3 - - -- TODO: disable code complete - - return bufnr -end - ----comment -M.restore_original_settings = function(bufnr) - if not bufnr then - error("No buffer found") - end - vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) - - local buf = M.buffers[bufnr] - if buf.buf_settings then - ---@diagnostic disable-next-line: undefined-field - vim.api.nvim_buf_set_option(bufnr, "spell", buf.buf_settings.spell) - ---@diagnostic disable-next-line: undefined-field - if buf.buf_settings.spell then - vim.cmd("set spell") - end - ---@diagnostic disable-next-line: undefined-field - if buf.buf_settings.hlsearch then - vim.cmd("set hlsearch") - end - ---@diagnostic disable-next-line: undefined-field - if buf.buf_settings.wrap then - vim.cmd("set wrap") - end - ---@diagnostic disable-next-line: undefined-field - vim.o.listchars = buf.buf_settings.listchars - vim.o.statusline = buf.buf_settings.status_line - vim.o.laststatus = buf.buf_settings.laststatus - end -end - return M diff --git a/lua/neohack/edits.lua b/lua/neohack/edits.lua index e9302c0..d79af9d 100644 --- a/lua/neohack/edits.lua +++ b/lua/neohack/edits.lua @@ -11,6 +11,8 @@ local utils = require("neohack.utils") local chance = require("neohack.chance") local event = require("neohack.event") local actions = require("neohack.actions") +local view_buffer = require("neohack.view_buffer") +local inventory = require("neohack.inventory") M.handle_insert_enter = function() state.insert_enter_start = vim.api.nvim_win_get_cursor(0) @@ -45,7 +47,7 @@ M.handle_insert = function() -- local row, col = buffer.get_under_cursor() -- if char ~= defs.floor.char then - -- local entity = player.retrieve_item_char(char) + -- local entity = inventory.retrieve_item_char(char) -- if not entity then -- -- undo the insert -- vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", true) @@ -72,10 +74,10 @@ M.handle_insert_exit = function() state.insert_enter_start = {} if #inserted > 0 then -- reset cursor position before performing any action - buffer.move_prev_cursor() + view_buffer.move_prev_cursor() for _, line in ipairs(inserted) do - actions.parse_action(line) + actions.execute_action(line) end end end @@ -120,7 +122,7 @@ M.handle_changed = function() for e_i, entity in ipairs(deleted_entities) do if entity.char == char then found = true - if not player.can_pickup(entity) then + if not inventory.can_pickup(entity) then all_items = false else table.insert(entities, entity) @@ -139,7 +141,7 @@ M.handle_changed = function() -- message.notify("Can't pickup non items '" .. deleted_chars .. "'") else for _, entity in ipairs(entities) do - player.pickup(entity) + inventory.pickup(entity) buffer.set_entity_at_cell(entity.row, entity.col, defs.new_floor(entity.row, entity.col)) -- TODO: do this once for all entities @@ -161,7 +163,7 @@ M.handle_changed = function() -- for col = startCol, endCol do -- local entity = buffer.get_entity_at_pos(row, col) -- if entity then - -- player.pickup(entity) + -- inventory.pickup(entity) -- -- fill in the deleted column in the next frame -- -- message.notify("replace pickup " .. i) -- end diff --git a/lua/neohack/event.lua b/lua/neohack/event.lua index f9adeea..256479f 100644 --- a/lua/neohack/event.lua +++ b/lua/neohack/event.lua @@ -15,6 +15,8 @@ local Entity = require("neohack.entity") local utils = require("neohack.utils") local chance = require("neohack.chance") local Move = require("neohack.move") +local view_buffer = require("neohack.view_buffer") +local inventory = require("neohack.inventory") ---comment ---@return boolean @@ -43,12 +45,12 @@ M.make_move = function(mover, move) if entity and entity.char == defs.floor.char and new_row > 0 and new_col > 0 then -- message.notify("making move " .. vim.inspect(mover) .. " " .. vim.inspect(move)) buffer.set_entity_at_cell(mover.row, mover.col, defs.new_floor(mover.row, mover.col)) - buffer.set_highlight(mover.row, mover.col, nil) + view_buffer.set_highlight(mover.row, mover.col, nil) buffer.set_entity_at_cell(new_row, new_col, mover) -- TODO: without this it leaves a hit to the enemy location if mover.visible then - buffer.set_highlight(new_row, new_col, Entity.enemy_highlight) + view_buffer.set_highlight(new_row, new_col, Entity.enemy_highlight) end return true else @@ -166,7 +168,7 @@ M.move_enemies = function() else M.move_closer_to(enemy, finalRow, finalCol, M.enemy_try_move) if enemy.hit_highlight then - buffer.highlight_hit(enemy.row, enemy.col, enemy.hit_highlight, 400) + view_buffer.highlight_hit(enemy.row, enemy.col, enemy.hit_highlight, 400) end enemy.hit_highlight = nil end @@ -186,7 +188,7 @@ M.move_friends = function() else M.move_closer_to(friend, finalRow, finalCol, M.friend_try_move) if friend.hit_highlight then - buffer.highlight_hit(friend.row, friend.col, friend.hit_highlight, 400) + view_buffer.highlight_hit(friend.row, friend.col, friend.hit_highlight, 400) end friend.hit_highlight = nil end @@ -254,7 +256,7 @@ M.say = function(words) local spell_names = "" ---@type Entity[] local casting = {} - for _, spell_item in pairs(player.get_spell_items()) do + for _, spell_item in pairs(inventory.get_spell_items()) do if string.find(word_str, spell_item.inscription.inscription) ~= nil then table.insert(casting, spell_item) spell_names = spell_names .. spell_item.inscription.inscription .. " " diff --git a/lua/neohack/fuse.lua b/lua/neohack/fuse.lua index 1079d9c..b38ddee 100644 --- a/lua/neohack/fuse.lua +++ b/lua/neohack/fuse.lua @@ -1,6 +1,6 @@ local M = {} -local Def = require("neohack.def") +local Entity = require("neohack.entity") local message = require("neohack.message") local chance = require("neohack.chance") @@ -12,7 +12,7 @@ local chance = require("neohack.chance") ---@param high_chance number ---@return Entity | nil function M.fuse(one, two, skill, low_chance, high_chance) - local n = setmetatable({}, Def) + local n = setmetatable({}, Entity) n.type = one.type n.block_vision = one.block_vision or two.block_vision n.char = M.average_chars(one.char, two.char) or one.char diff --git a/lua/neohack/game.lua b/lua/neohack/game.lua index dd3d379..e9f27cc 100644 --- a/lua/neohack/game.lua +++ b/lua/neohack/game.lua @@ -1,4 +1,5 @@ local tick_timer = require("neohack.tick_timer") +local view_buffer = require("neohack.view_buffer") --- the overall game --- @@ -134,7 +135,7 @@ M.end_game = function() for bufnr, _ in pairs(buffer.buffers) do local _, _, first_floor = map.scan_buf(bufnr) vim.api.nvim_set_current_buf(bufnr) - buffer.set_cursor(first_floor.row, first_floor.col) + view_buffer.set_cursor(first_floor.row, first_floor.col) map.all_visible(bufnr) end buffer.end_game() diff --git a/lua/neohack/generated_defs.lua b/lua/neohack/generated_defs.lua index 98ac53b..cc1523b 100644 --- a/lua/neohack/generated_defs.lua +++ b/lua/neohack/generated_defs.lua @@ -182,6 +182,7 @@ M.generate_enemy = function() return new_def end +-- TODO: finish or delete local function generate_spell() return function(entity) -- Get all the fields of the entity diff --git a/lua/neohack/inventory.lua b/lua/neohack/inventory.lua new file mode 100644 index 0000000..3a04702 --- /dev/null +++ b/lua/neohack/inventory.lua @@ -0,0 +1,128 @@ +--- player inventory +--- + +local utils = require("neohack.utils") +local Def = require("neohack.def") +local defs = require("neohack.defs") +local state = require("neohack.state") +local message = require("neohack.message") + +local M = {} + +---comment +---@param entity Entity +---@return boolean +M.can_pickup = function(entity) + return entity.type == Def.DefType.item or entity.type == Def.DefType.floor +end + +---comment +---@param entity Entity +M.pickup = function(entity) + if entity.char == defs.player_corpse then + M.pickup_player_corpse() + elseif entity.type == Def.DefType.item then + M.pickup_item(entity) + else + -- do nothing + end +end + +--- collect items from your corpse +M.pickup_player_corpse = function() + for _, value in ipairs(state.corpse.items) do + table.insert(state.player.items, value) + end + state.corpse = { row = nil, col = nil, items = {} } + message.notify("Got your corpse.") +end + +--- pick up item +---@param item Entity +M.pickup_item = function(item) + table.insert(state.player.items, 1, item) + message.notify("Got a " .. item.name) +end + +---comment +---@return string +M.get_inventory_item_with_index = function() + local items_str = "" + for index, item in pairs(state.player.items) do + items_str = items_str .. index .. ":" .. item.name .. "\n" + end + return items_str +end + +---comment +---comment +---@return Entity[] +M.get_spell_items = function() + local spells = {} + for _, item in pairs(state.player.items) do + if item.inscription then + table.insert(spells, item) + end + end + return spells +end + +---drop the oldest item that matches char +---@param char string +---@return Entity? +M.retrieve_item_char = function(char) + for i = #state.player.items, 1, -1 do + if state.player.items[i].char == char then + local entity = table.remove(state.player.items, i) + -- TODO: deal with retrieve for action being too verbose + message.notify("Retrieved " .. entity.char) + return entity + end + end + message.notify("No " .. char .. " to retrieve") + return nil +end + +---get the named item out of inventory +---@param keys string[] names or indexes +---@return Entity[]? +M.retrieve_items = function(keys) + local items = {} + local indexes = {} + for _, name in ipairs(keys) do + local found = false + local index = utils.string_to_int(name) + if index then + table.insert(items, state.player.items[index]) + table.insert(indexes, index) + found = true + else + for i, v in ipairs(state.player.items) do + if v.name == name then + -- matches first found + table.insert(items, v) + table.insert(indexes, i) + found = true + break + end + end + end + if not found then + message.notify("No " .. name .. " to retrieve") + -- get all or nothing + return nil + end + end + + -- highest index first, so they all get removed + table.sort(indexes, function(a, b) + return a > b + end) + for _, index in ipairs(indexes) do + table.remove(state.player.items, index) + end + -- message.notify("Retrieved " .. table.concat(indexes, " ")) + return items +end + +return M diff --git a/lua/neohack/level.lua b/lua/neohack/level.lua deleted file mode 100644 index ab56b60..0000000 --- a/lua/neohack/level.lua +++ /dev/null @@ -1,6 +0,0 @@ -local M = { - bufnr = nil, - level = nil, -} - -return M diff --git a/lua/neohack/map.lua b/lua/neohack/map.lua index d017f98..5b3df52 100644 --- a/lua/neohack/map.lua +++ b/lua/neohack/map.lua @@ -8,6 +8,7 @@ local defs = require("neohack.defs") local buffer = require("neohack.buffer") local message = require("neohack.message") local state = require("neohack.state") +local view_buffer = require("neohack.view_buffer") --- scan the buf, looking for points of interest ---@return Entity[] enemies @@ -135,7 +136,7 @@ M.show_visible_line_of_sight = function(bufnr, view_distance) then entity.visible = false -- setting highlights individually for non visible chars hurts performance a lot - -- buffer.set_highlight(row_index, col_index, defs.hidden_highlight) + -- view_buffer.set_highlight(row_index, col_index, defs.hidden_highlight) else entity.visible = true if entity.type == Def.DefType.terrain then @@ -143,7 +144,7 @@ M.show_visible_line_of_sight = function(bufnr, view_distance) end local hi = entity:get_highlight() if hi then - buffer.set_highlight(row_index, col_index, hi) + view_buffer.set_highlight(row_index, col_index, hi) end end end @@ -219,7 +220,7 @@ end ---@param bufnr integer ---@return Entity[][] M.find_deleted = function(bufnr) - local new_buffer = buffer.read_a_buf(bufnr) + local new_buffer = view_buffer.read_a_buf(bufnr) local old_buffer = buffer.buffers[bufnr].cells -- TODO: this doesn't work properly on the last line while #new_buffer < #old_buffer do @@ -249,7 +250,7 @@ M.find_deleted_positions = function(old_buffer, new_buffer) elseif new_buffer[row][col].char ~= defs.not_visible and new_buffer[row][col].char ~= old_buffer[row][col].char then - message.notify(old_buffer[row][col].char .. " vs " .. new_buffer[row][col].char) + -- message.notify(old_buffer[row][col].char .. " vs " .. new_buffer[row][col].char) table.insert(deleted_positions, old_buffer[row][col]) end else @@ -355,7 +356,7 @@ end ---@param entities Entity[] ---@return boolean if entities are all the same in the real view buffer M.present_in_buffer = function(entities) - local cells = buffer.read_buf_chars(state.current_bufnr) + local cells = view_buffer.read_buf_chars(state.current_bufnr) for _, entity in ipairs(entities) do local char = cells[entity.row][entity.col] if char ~= entity.char then diff --git a/lua/neohack/message.lua b/lua/neohack/message.lua index 4a11b25..0a917f4 100644 --- a/lua/neohack/message.lua +++ b/lua/neohack/message.lua @@ -12,9 +12,10 @@ M.open = function() end ---comment ----@param message string -M.notify = function(message) - M.notify_func(message) +---comment +---@param ... any +M.notify = function(...) + M.notify_func(...) end ---comment @@ -31,8 +32,10 @@ local function split_string_into_lines(s) end ---comment ----@param message string -M.send_to_message_buf = function(message) +---comment +---@param ... any +M.send_to_message_buf = function(...) + local message = table.concat({ ... }, " ") vim.schedule(function() vim.api.nvim_buf_set_lines(M.message_buf, -1, -1, false, split_string_into_lines(message)) local win_ids = vim.fn.win_findbuf(M.message_buf) diff --git a/lua/neohack/player.lua b/lua/neohack/player.lua index fd5d353..f99f524 100644 --- a/lua/neohack/player.lua +++ b/lua/neohack/player.lua @@ -1,7 +1,6 @@ --- player stuff --- -local utils = require("neohack.utils") local fuse = require("neohack.fuse") local Def = require("neohack.def") local defs = require("neohack.defs") @@ -10,6 +9,8 @@ local state = require("neohack.state") local message = require("neohack.message") local Entity = require("neohack.entity") local chance = require("neohack.chance") +local view_buffer = require("neohack.view_buffer") +local inventory = require("neohack.inventory") local M = { ---@type number required to search a corpse @@ -45,7 +46,7 @@ M.player_sneak_move = function() if chance.action_success(M.sneak_skill(), 0, entity.vision, M.sneak_threshold) then message.notify("Sneaked past a " .. entity.name) else - buffer.move_prev_cursor() + view_buffer.move_prev_cursor() message.notify("Sneaked failed") end M.store_previous_position() @@ -64,7 +65,7 @@ M.player_hit_move = function() elseif entity.type == Def.DefType.floor then -- do nothing elseif entity.type == Def.DefType.friend then - buffer.move_prev_cursor() + view_buffer.move_prev_cursor() entity.health = entity.health + 0.5 message.notify("Hugged " .. entity.name) elseif entity.type == Def.DefType.enemy then @@ -87,34 +88,6 @@ M.store_previous_position = function() state.prev_cursor = { row = prevRow, col = prevCol + 1 } end ----comment ----@param entity Entity ----@return boolean -M.can_pickup = function(entity) - return entity.type == Def.DefType.item or entity.type == Def.DefType.floor -end - ----comment ----@param entity Entity -M.pickup = function(entity) - if entity.char == defs.player_corpse then - M.pickup_player_corpse() - elseif entity.type == Def.DefType.item then - M.pickup_item(entity) - else - -- do nothing - end -end - ---- collect items from your corpse -M.pickup_player_corpse = function() - for _, value in ipairs(state.corpse.items) do - table.insert(state.player.items, value) - end - state.corpse = { row = nil, col = nil, items = {} } - message.notify("Got your corpse.") -end - ---comment ---@param entity Entity ---@return Entity | nil @@ -128,13 +101,6 @@ M.enemy_drop_item = function(entity) end end ---- pick up item ----@param item Entity -M.pickup_item = function(item) - table.insert(state.player.items, 1, item) - message.notify("Got a " .. item.name) -end - --- bounce off enemies, unless you kill them ---@param row integer ---@param col integer @@ -146,7 +112,7 @@ M.hit_enemy = function(row, col) if chance.action_success(M.weapon_skill(), weapon.hit_rate, enemy.vision, M.hit_threshold) then M.hit_enemy_success(enemy, weapon) else - buffer.move_prev_cursor() + view_buffer.move_prev_cursor() enemy.hit_highlight = defs.missing_highlight message.notify("Missed " .. enemy.name) end @@ -167,7 +133,7 @@ M.hit_enemy_success = function(enemy, weapon) message.notify("Killed " .. enemy.name .. " with a " .. weapon.name .. " for " .. damage) else enemy.hit_highlight = defs.hitting_highlight - buffer.move_prev_cursor() + view_buffer.move_prev_cursor() message.notify("Hit " .. enemy.name .. " with a " .. weapon.name .. " for " .. damage) end @@ -182,9 +148,9 @@ end ---@param col integer ---@param terrain Entity M.hit_terrain = function(row, col, terrain) - buffer.highlight_hit(row, col, defs.hitting_highlight, 150) + view_buffer.highlight_hit(row, col, defs.hitting_highlight, 150) -- bounce off terrain - buffer.move_prev_cursor() + view_buffer.move_prev_cursor() if terrain.name == "down" then local level = M.handle_down() message.notify("Went down to " .. level) @@ -202,114 +168,33 @@ M.hit_terrain = function(row, col, terrain) end end ----comment ----@return Entity -M.get_weapon = function() - return state.player.slots[state.slots.right_hand] or Entity.new(defs.slap, 0, 0) -end - -M.get_wearing = function() - local wearing = "" - for name, char in pairs(state.slots) do - local item = state.player.slots[char] - local item_name = item and item.name or "" - wearing = wearing .. name .. ":" .. item_name .. " " - end - return wearing -end - ---comment ---@return string -M.show_inventory = function() +M.get_inventory = function() local p = state.player local parts = {} for _, body in pairs(p.bodies) do table.insert(parts, body.name) end local bodies = table.concat(parts, " ") - local items_str = M.get_inventory_item_with_index() + local items_str = inventory.get_inventory_item_with_index() return "wearing: " .. M.get_wearing() .. "\nitems:\n" .. items_str .. "\nkills: " .. bodies end ---comment ----@return string -M.get_inventory_item_with_index = function() - local items_str = "" - for index, item in pairs(state.player.items) do - items_str = items_str .. index .. ":" .. item.name .. "\n" - end - return items_str -end - ----comment ----comment ----@return Entity[] -M.get_spell_items = function() - local spells = {} - for _, item in pairs(state.player.items) do - if item.inscription then - table.insert(spells, item) - end - end - return spells -end - ----drop the oldest item that matches char ----@param char string ----@return Entity? -M.retrieve_item_char = function(char) - for i = #state.player.items, 1, -1 do - if state.player.items[i].char == char then - local entity = table.remove(state.player.items, i) - -- TODO: deal with retrieve for action being too verbose - message.notify("Retrieved " .. entity.char) - return entity - end - end - message.notify("No " .. char .. " to retrieve") - return nil +---@return Entity +M.get_weapon = function() + return state.player.slots[state.slots.right_hand] or Entity.new(defs.slap, 0, 0) end ----get the named item out of inventory ----@param keys string[] names or indexes ----@return Entity[]? -M.retrieve_items = function(keys) - local items = {} - local indexes = {} - for _, name in ipairs(keys) do - local found = false - local index = utils.string_to_int(name) - if index then - table.insert(items, state.player.items[index]) - table.insert(indexes, index) - found = true - else - for i, v in ipairs(state.player.items) do - if v.name == name then - -- matches first found - table.insert(items, v) - table.insert(indexes, i) - found = true - break - end - end - end - if not found then - message.notify("No " .. name .. " to retrieve") - -- get all or nothing - return nil - end - end - - -- highest index first, so they all get removed - table.sort(indexes, function(a, b) - return a > b - end) - for _, index in ipairs(indexes) do - table.remove(state.player.items, index) +M.get_wearing = function() + local wearing = "" + for name, char in pairs(state.slots) do + local item = state.player.slots[char] + local item_name = item and item.name or "" + wearing = wearing .. name .. ":" .. item_name .. " " end - -- message.notify("Retrieved " .. table.concat(indexes, " ")) - return items + return wearing end ---comment @@ -332,7 +217,7 @@ M.wear = function(key, slot) if key == "0" or key == "nothing" then return end - local items = M.retrieve_items({ key }) + local items = inventory.retrieve_items({ key }) if items and items[1] then p.slots[slot] = items[1] message.notify("Wearing " .. items[1].name .. " on " .. state.slot_name(slot)) @@ -344,7 +229,7 @@ end ---comment ---@param keys string[] M.fuse = function(keys) - local items = M.retrieve_items(keys) + local items = inventory.retrieve_items(keys) if not items then return end @@ -360,7 +245,7 @@ M.fuse = function(keys) end local new_item = Entity.new(new_def, 0, 0) message.notify("Fused " .. table.concat(keys, " ") .. " into " .. new_item.name) - M.pickup_item(new_item) + inventory.pickup_item(new_item) end ---comment @@ -372,13 +257,13 @@ M.look = function(keys) table.remove(keys, i) end end - local items = M.retrieve_items(keys) + local items = inventory.retrieve_items(keys) if not items then return end for _, item in ipairs(items) do if item then - M.pickup_item(item) + inventory.pickup_item(item) message.notify("Looked at " .. item:look()) end end @@ -387,7 +272,7 @@ end ---comment ---@param keys string[] M.eat = function(keys) - local items = M.retrieve_items(keys) + local items = inventory.retrieve_items(keys) if not items then return end @@ -417,7 +302,7 @@ end ---comment ---@param keys string[] M.drop = function(keys) - local items = M.retrieve_items(keys) + local items = inventory.retrieve_items(keys) if not items then return end @@ -437,10 +322,10 @@ end ---comment ---@return boolean true if player is now dead M.check_dead = function(row, col) - if state.player.health <= 0 then + if not state.player.alive or state.player.health <= 0 then -- TODO: fix not ending game when we die state.player.alive = false - message.notify("YOU DIED " .. M.show_inventory()) + message.notify("YOU DIED " .. M.get_inventory()) local items = state.player.items for _, item in ipairs(state.player.slots) do table.insert(items, item) @@ -462,14 +347,15 @@ M.hit_by = function(attacker, row, col) buffer.highlight_cursor(defs.cursor_dodge_highlight) M.dodge(attacker) else - local damage = state.player.health + M.deflect_skill() - attacker.damage + local damage = attacker.damage - M.deflect_skill() + -- message.notify("damage calc " .. state.player.health .. " " .. M.deflect_skill() .. " " .. attacker.damage) if damage > 0 then - state.player.health = damage + state.player.health = state.player.health - damage -- TODO: hit player highlight not working -- highlight is set, but cursor overrides it buffer.highlight_cursor(defs.cursor_hit_highlight) - buffer.highlight_hit(attacker.row, attacker.col, defs.hit_by_highlight, 200) - message.notify("Hurt by " .. attacker.name .. " for " .. attacker.damage) + view_buffer.highlight_hit(attacker.row, attacker.col, defs.hit_by_highlight, 200) + message.notify("Hurt by " .. attacker.name .. " for " .. damage) M.check_dead(row, col) else diff --git a/lua/neohack/utils.lua b/lua/neohack/utils.lua index 5292c85..9d21abd 100644 --- a/lua/neohack/utils.lua +++ b/lua/neohack/utils.lua @@ -113,4 +113,8 @@ M.split_lines = function(chars) return patterns end +M.capitalize_first_letter = function(str) + return str:gsub("^%l", string.upper) +end + return M diff --git a/lua/neohack/view_buffer.lua b/lua/neohack/view_buffer.lua new file mode 100644 index 0000000..13e82e5 --- /dev/null +++ b/lua/neohack/view_buffer.lua @@ -0,0 +1,228 @@ +--- all the interactions with the actual neovim buffer that is seen, ie the view +--- + +local M = { + AutoCmdGroup = "NeoHackGameTick", +} + +local defs = require("neohack.defs") +local state = require("neohack.state") + +local namespace = vim.api.nvim_create_namespace("NeoHack") +local group = vim.api.nvim_create_augroup(M.AutoCmdGroup, { clear = true }) + +--- Copy the current buffer and return the new buffer number +---@return integer bufnr +M.copy_buffer = function() + local current_bufnr = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(current_bufnr, 0, -1, false) + return M.new_buffer(lines) +end + +---comment +---@param lines string[] +---@return integer +M.new_buffer = function(lines) + local new_bufnr = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_lines(new_bufnr, 0, -1, false, lines) + vim.api.nvim_set_current_buf(new_bufnr) + return new_bufnr +end + +M.setup_buffer = function(bufnr, first_floor) + vim.api.nvim_set_current_buf(bufnr) + if first_floor then + M.set_cursor(first_floor.row, first_floor.col) + end +end + +--- Remove handlers for a specific buffer +---@param bufnr integer +M.remove_handlers = function(bufnr) + vim.api.nvim_clear_autocmds({ group = M.AutoCmdGroup, buffer = bufnr }) +end + +M.add_handler = function(buffer, event, func) + vim.api.nvim_create_autocmd(event, { + group = group, + buffer = buffer.bufnr, + callback = func, + }) +end + +--TODO: is this actually used now that maps are generated? it creates entities from the existing chars +--- Read buffer content and update the cells for a specific buffer +---@param bufnr integer +---@return Entity[][] +M.read_a_buf = function(bufnr) + local new_cells = {} -- prepare a new frame + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + for row, value in ipairs(lines) do + local result = {} + for col = 1, #value do + local char = value:sub(col, col) + result[col] = defs.new_entity_from_char(char, row, col) + end + new_cells[row] = result + end + return new_cells +end + +--- Read buffer content as a grid of chars +---@param bufnr integer +---@return string[][] +M.read_buf_chars = function(bufnr) + local new_cells = {} -- prepare a new frame + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + for row, value in ipairs(lines) do + local result = {} + for col = 1, #value do + local char = value:sub(col, col) + result[col] = char + end + new_cells[row] = result + end + return new_cells +end + +--- Write the buffer content based on the cells for a specific buffer +---comment +---@param bufnr integer +---@param cells Entity[][] +M.write_buf = function(bufnr, cells) + local lines = {} + for row_index, row in ipairs(cells) do + local value = {} + for _, col in ipairs(row) do + if col.visible then + table.insert(value, col.char) + elseif col.seen then + table.insert(value, col.char) + else + table.insert(value, defs.not_visible) + end + end + lines[row_index] = table.concat(value) + end + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) +end + +--- Set highlight for a specific position in a specific buffer +---@param row integer +---@param col integer +---@param highlight string? +M.set_highlight = function(row, col, highlight) + local bufnr = state.current_bufnr + if not bufnr then + error("no bufnr") + end + if highlight then + vim.defer_fn(function() + vim.api.nvim_buf_add_highlight(bufnr, namespace, highlight, row - 1, col - 1, col) + end, 1) + end +end +--- Highlight hit at a specific position in a specific buffer +---@param row integer +---@param col integer +---@param highlight string +---@param timeout integer +M.highlight_hit = function(row, col, highlight, timeout) + local bufnr = state.current_bufnr + if not bufnr then + error("no bufnr") + end + M.set_highlight(row, col, highlight) + vim.defer_fn(function() + vim.api.nvim_buf_clear_namespace(bufnr, namespace, row - 1, row) + end, timeout) +end + +---comment +---@param row integer +---@param col integer +M.set_cursor = function(row, col) + vim.api.nvim_win_set_cursor(0, { row, col - 1 }) +end + +---comment +M.move_prev_cursor = function() + -- TODO: show the bouncing effect using smear-cursor + -- vim.defer_fn(function() + M.set_cursor(state.prev_cursor.row, state.prev_cursor.col) + -- end, 10) +end + +-- TODO: do we need to save/restore buffer settings anymore with a copy of the buffer? +---comment +---@param bufnr integer +---@param buffer Buffer +---@return integer bufnr +M.setup_game_buffer = function(bufnr, buffer) + local winnr = 0 + if not bufnr then + error("No buffer found") + end + buffer.buf_settings = { + spell = vim.wo[winnr].spell, + hlsearch = vim.api.nvim_get_option("hlsearch"), + wrap = vim.wo[winnr].wrap, + listchars = vim.o.listchars, + cursorline_hl = vim.api.nvim_get_hl(0, { name = "CursorLine", link = false }), + cursorcolumn_hl = vim.api.nvim_get_hl(0, { name = "CursorColumn", link = false }), + statusline = vim.o.statusline, + laststatus = vim.o.laststatus, + } + + vim.api.nvim_buf_clear_namespace(bufnr, -1, 0, -1) + + vim.api.nvim_buf_set_option(bufnr, "cursorline", true) + vim.api.nvim_buf_set_option(bufnr, "cursorcolumn", true) + + vim.api.nvim_buf_set_option(bufnr, "spell", false) + vim.cmd.nohlsearch() + vim.cmd("set nowrap") + vim.o.listchars = "" + + -- set the statusline to show in game info + vim.o.statusline = "%!v:lua.require'neohack'.status_line()" + vim.o.laststatus = 3 + + -- TODO: disable code complete + + return bufnr +end + +---comment +---comment +---@param bufnr integer +---@param buffer Buffer +M.restore_original_settings = function(bufnr, buffer) + if not bufnr then + error("No buffer found") + end + vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) + + if buffer.buf_settings then + ---@diagnostic disable-next-line: undefined-field + vim.api.nvim_buf_set_option(bufnr, "spell", buffer.buf_settings.spell) + ---@diagnostic disable-next-line: undefined-field + if buffer.buf_settings.spell then + vim.cmd("set spell") + end + ---@diagnostic disable-next-line: undefined-field + if buffer.buf_settings.hlsearch then + vim.cmd("set hlsearch") + end + ---@diagnostic disable-next-line: undefined-field + if buffer.buf_settings.wrap then + vim.cmd("set wrap") + end + ---@diagnostic disable-next-line: undefined-field + vim.o.listchars = buffer.buf_settings.listchars + vim.o.statusline = buffer.buf_settings.status_line + vim.o.laststatus = buffer.buf_settings.laststatus + end +end + +return M diff --git a/tests/action_spec.lua b/tests/action_spec.lua new file mode 100644 index 0000000..747f538 --- /dev/null +++ b/tests/action_spec.lua @@ -0,0 +1,54 @@ +local actions = require("neohack.actions") +local match = require("luassert.match") +local message = require("neohack.message") + +---@diagnostic disable-next-line: undefined-field +local eq = assert.is.equal +local not_nil = match.is_not_nil + +actions.leader_key = " " +actions.tick = function() + print("tick called") +end +message.notify_func = print + +describe("parse_action", function() + it("handles nil", function() + ---@diagnostic disable-next-line: param-type-mismatch + local action, request = actions.parse_action(nil) + eq(nil, action) + eq(nil, request) + end) + + it("handles empty", function() + local action, request = actions.parse_action("") + eq(nil, action) + eq(nil, request) + end) + + it("handles spaces", function() + local action, request = actions.parse_action(" ") + eq(nil, action) + eq(nil, request) + end) + + it("handles help", function() + local action, request = actions.parse_action("help") + eq(actions.actions.help, action) + match.equal({ action = "help" }, request) + end) + + it("handles help spaces", function() + local action, request = actions.parse_action(" help ") + eq(actions.actions.help, action) + match.equal({ action = "help" }, request) + end) + + it("handles help synonyms", function() + eq(actions.actions.help, actions.parse_action("h")) + eq(actions.actions.help, actions.parse_action("?")) + eq(actions.actions.help, actions.parse_action("-h")) + eq(actions.actions.help, actions.parse_action("-help")) + eq(actions.actions.help, actions.parse_action("--help")) + end) +end) diff --git a/tests/chat_spec.lua b/tests/chat_spec.lua new file mode 100644 index 0000000..dfc7815 --- /dev/null +++ b/tests/chat_spec.lua @@ -0,0 +1,67 @@ +local match = require("luassert.match") +local message = require("neohack.message") +local chat = require("neohack.chat") + +---@diagnostic disable-next-line: undefined-field +local eq = assert.is.equal +local not_nil = match.is_not_nil + +message.notify_func = print + +describe("parse_request", function() + it("handles nil", function() + ---@diagnostic disable-next-line: param-type-mismatch + match.equal(nil, chat.parse_request(nil)) + end) + + it("handles empty", function() + match.equal(nil, chat.parse_request("")) + end) + + it("handles spaces", function() + match.equal(nil, chat.parse_request(" ")) + end) + + it("handles one word", function() + match.equal({ action = "a" }, chat.parse_request("a")) + match.equal({ action = "a" }, chat.parse_request(" a ")) + match.equal({ action = "a" }, chat.parse_request("a ")) + match.equal({ action = "a" }, chat.parse_request(" a")) + match.equal({ action = "action" }, chat.parse_request(" action ")) + end) + + it("handles two words", function() + match.equal({ action = "a", object = "o" }, chat.parse_request("a o")) + match.equal({ action = "action", object = "object" }, chat.parse_request("action object")) + end) + + it("handles three words", function() + match.equal({ action = "a", object = "o", target = "t" }, chat.parse_request("a o t")) + match.equal({ action = "action", object = "object", target = "target" }, chat.parse_request("action object target")) + match.equal( + { action = "action", object = "object", target = "target" }, + chat.parse_request(" action object target ") + ) + end) +end) + +describe("no_action", function() + -- TODO: not great tests, should test a return value + + it("handles empty", function() + ---@diagnostic disable-next-line: missing-fields + chat.no_action({}) + end) + + it("handles one word", function() + chat.no_action({ action = "a" }) + end) + + it("handles two words", function() + chat.no_action({ action = "a", object = "o" }) + end) + + it("handles three words", function() + chat.no_action({ action = "a", object = "o", target = "t" }) + end) +end) diff --git a/tests/fuse_spec.lua b/tests/fuse_spec.lua new file mode 100644 index 0000000..be4369c --- /dev/null +++ b/tests/fuse_spec.lua @@ -0,0 +1,41 @@ +local match = require("luassert.match") +local message = require("neohack.message") +local fuse = require("neohack.fuse") +local defs = require("neohack.defs") + +---@diagnostic disable-next-line: undefined-field +local eq = assert.is.equal +local not_nil = match.is_not_nil + +message.notify_func = print + +describe("fuse", function() + it("fails", function() + local one = defs.new_entity_from_char("!", 1, 1) + local two = defs.new_entity_from_char("l", 2, 2) + local result = fuse.fuse(one, two, -100, 0, 0) + eq(nil, result) + end) + + it("low", function() + local one = defs.new_entity_from_char("!", 1, 1) + local two = defs.new_entity_from_char("l", 2, 2) + local result = fuse.fuse(one, two, 0.01, 0, 0) + not_nil(result) + if result then + eq("F", result.char) + eq(3, result.damage) + end + end) + + it("high", function() + local one = defs.new_entity_from_char("!", 1, 1) + local two = defs.new_entity_from_char("l", 2, 2) + local result = fuse.fuse(one, two, 100, 0.001, 0.1) + not_nil(result) + if result then + eq("F", result.char) + eq(5, result.damage) + end + end) +end) diff --git a/tests/inventory_spec.lua b/tests/inventory_spec.lua new file mode 100644 index 0000000..bdb8a84 --- /dev/null +++ b/tests/inventory_spec.lua @@ -0,0 +1,73 @@ +local inventory = require("neohack.inventory") +local message = require("neohack.message") +local defs = require("neohack.defs") +local state = require("neohack.state") +local Entity = require("neohack.entity") +local generated_defs = require("neohack.generated_defs") + +local match = require("luassert.match") +---@diagnostic disable-next-line: undefined-field +local eq = assert.is.equal +local not_nil = match.is_not_nil + +message.notify_func = print + +describe("can_pickup", function() + it("can", function() + eq(true, inventory.can_pickup(defs.new_entity_from_char("!", 1, 1))) + eq(true, inventory.can_pickup(defs.new_entity_from_char(" ", 1, 1))) + end) + + it("cannot", function() + eq(false, inventory.can_pickup(defs.new_entity_from_char("A", 1, 1))) + end) +end) + +describe("pickup", function() + it("get_inventory_item_with_index", function() + state.player.items = {} + local item = defs.new_entity_from_char("!", 1, 1) + inventory.pickup(item) + eq(item, state.player.items[1]) + eq("1:long_sword\n", inventory.get_inventory_item_with_index()) + end) + + it("get_spell_items", function() + state.player.items = {} + state.current_floor = 1 + generated_defs.init() + + local item = Entity.new(defs.tome, 1, 1) + item.inscription = generated_defs.generate_spell() + inventory.pickup(item) + eq(item, state.player.items[1]) + match.equal({}, inventory.get_spell_items()) + end) + + it("retrieve_item_char", function() + state.player.items = {} + local item = defs.new_entity_from_char("!", 1, 1) + inventory.pickup(item) + eq(item, state.player.items[1]) + match.equal(item, inventory.retrieve_item_char("!")) + end) + + it("retrieve_items", function() + state.player.items = {} + local item = defs.new_entity_from_char("!", 1, 1) + inventory.pickup(item) + eq(item, state.player.items[1]) + match.equal(item, inventory.retrieve_items({ "long_sword" })[1]) + end) +end) + +describe("pickup_player_corpse", function() + it("get_inventory_item_with_index", function() + state.player.items = {} + local item = defs.new_entity_from_char("!", 1, 1) + state.corpse.items = { item } + inventory.pickup_player_corpse() + eq(item, state.player.items[1]) + eq("1:long_sword\n", inventory.get_inventory_item_with_index()) + end) +end) diff --git a/tests/map_spec.lua b/tests/map_spec.lua index 01fe722..6c52675 100644 --- a/tests/map_spec.lua +++ b/tests/map_spec.lua @@ -4,18 +4,36 @@ local Entity = require("neohack.entity") local message = require("neohack.message") local buffer = require("neohack.buffer") local state = require("neohack.state") +local Def = require("neohack.def") local match = require("luassert.match") ---@diagnostic disable-next-line: undefined-field local eq = assert.is.equal local not_nil = match.is_not_nil +message.notify_func = print + +local enemy = "E" +local friend = "F" + ---comment ---@param name string ---@return Entity local function newE(name, row, col) + local type = Def.DefType.item + if name == enemy or name == "x" then + type = Def.DefType.enemy + elseif name == friend then + type = Def.DefType.friend + elseif name == " " then + type = Def.DefType.floor + end ---@diagnostic disable-next-line: missing-fields - return Entity.new({ char = name, name = "entity " .. name }, row, col) + return Entity.new({ + char = name, + name = "entity " .. name, + type = type, + }, row, col) end ---comment @@ -33,20 +51,32 @@ local function toEntities(chars) return entities end +---comment +---@param expected table[] +---@param found table[]? local function assertFound(expected, found) not_nil(found) if found then eq(#expected, #found) for i, e in ipairs(expected) do - eq(e[1], found[i].row) - eq(e[2], found[i].col) + eq(e[1], found[i].row, e[1] .. " " .. e[2]) + eq(e[2], found[i].col, e[1] .. " " .. e[2]) end end end -describe("find_deleted_positions", function() - message.notify_func = print +local bufnr = 1 +---comment +---@param chars string[][] +local function set_buffer(chars) + state.current_bufnr = bufnr + ---@diagnostic disable-next-line: missing-fields + buffer.buffers[bufnr] = { + cells = toEntities(chars), + } +end +describe("find_deleted_positions", function() it("finds deleted", function() local old = { { "a", "b", "c" }, @@ -104,8 +134,6 @@ local function as_chars(result) end describe("find_all_matches", function() - message.notify_func = print - it("finds single char", function() local cells = { { "a", "b", "c" }, @@ -243,20 +271,7 @@ describe("find_all_matches", function() end) end) -local bufnr = 1 ----comment ----@param chars string[][] -local function set_buffer(chars) - state.current_bufnr = bufnr - ---@diagnostic disable-next-line: missing-fields - buffer.buffers[bufnr] = { - cells = toEntities(chars), - } -end - describe("find_closest_match", function() - message.notify_func = print - it("finds single char", function() local cells = { { "a", "b", "c" }, @@ -324,3 +339,41 @@ describe("find_closest_match", function() }, found_last) end) end) + +describe("scan_buf", function() + it("finds things", function() + local cells = { + { "F", "x", " ", " ", "E" }, + { " ", "E", " ", "F", " " }, + { " ", " ", "F", " ", " " }, + { " ", "E", " ", "F", " " }, + { "F", " ", " ", "x", "F" }, + } + set_buffer(cells) + + local enemies, friends, first_floor = map.scan_buf(bufnr) + match.equal({ row = 1, col = 3 }, first_floor) + assertFound({ { 1, 5 }, { 2, 2 }, { 4, 2 } }, enemies) + assertFound({ { 1, 1 }, { 2, 4 }, { 3, 3 }, { 4, 4 }, { 5, 1 }, { 5, 5 } }, friends) + end) +end) + +describe("scan_area", function() + it("finds things", function() + local cells = { + { "I", " ", " ", "x", "E" }, + { " ", "E", " ", "I", " " }, + { "x", " ", "I", " ", " " }, + { " ", "E", " ", "I", " " }, + { "I", " ", " ", " ", "I" }, + } + set_buffer(cells) + local row = 3 + local col = 3 + + local enemies, items = map.scan_area(bufnr, row, col, 1) + + assertFound({ { 2, 2 }, { 4, 2 } }, enemies) + assertFound({ { 2, 4 }, { 3, 3 }, { 4, 4 } }, items) + end) +end)