Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cf1d8a2
test: add comprehensive tests for DijkstraSearch and KoshienMock
takaokouji Oct 5, 2025
5620a18
test: add comprehensive tests for Koshien API methods
takaokouji Oct 5, 2025
9dbe987
test: add comprehensive calc_route tests for all map cell types
takaokouji Oct 5, 2025
abdcd2c
refactor: remove duplicate DijkstraSearch module from koshien.rb
takaokouji Oct 5, 2025
ef664d7
test: add comprehensive tests for Koshien::Map class
takaokouji Oct 5, 2025
05d6875
test: add comprehensive tests for Koshien::Position class
takaokouji Oct 5, 2025
2a63002
test: add comprehensive tests for List class
takaokouji Oct 5, 2025
54d9c98
test: add comprehensive tests for Smalruby3::World class
takaokouji Oct 5, 2025
6a91712
test: add comprehensive tests for Smalruby3::IgnoreMethodMissing class
takaokouji Oct 5, 2025
8a9b016
test: add comprehensive tests for Smalruby3::Stage class
takaokouji Oct 6, 2025
c368ff4
test: add comprehensive tests for Smalruby3::Sprite class
takaokouji Oct 6, 2025
fdc213d
test: improve List#index coverage to handle nil return value
takaokouji Oct 6, 2025
fb3972b
test: add tests for Koshien#position method
takaokouji Oct 6, 2025
39d8cfc
test: add tests for Koshien#position_of_x method
takaokouji Oct 6, 2025
e9617d7
test: add tests for Koshien#position_of_y method
takaokouji Oct 6, 2025
cc3f8af
test: add tests for Koshien goal methods
takaokouji Oct 6, 2025
0f7fcee
test: add tests for Koshien player methods
takaokouji Oct 6, 2025
1dcf214
test: add comprehensive tests for Koshien#object method
takaokouji Oct 6, 2025
efc7cb1
test: add tests for Koshien other_player methods
takaokouji Oct 6, 2025
a825e85
test: add tests for Koshien enemy methods
takaokouji Oct 6, 2025
2a9d084
test: add tests for Koshien#set_message method
takaokouji Oct 6, 2025
fbb180d
test: add tests for Koshien#locate_objects method
takaokouji Oct 6, 2025
3b1bb2d
test: add tests for Koshien action management methods
takaokouji Oct 6, 2025
faeec73
test: add tests for Koshien private helper methods
takaokouji Oct 6, 2025
3571c83
test: add tests for Koshien#map method
takaokouji Oct 6, 2025
884f347
test: enhance tests for Koshien#map_all method
takaokouji Oct 6, 2025
053a4b1
test: add tests for Koshien#get_map_area method
takaokouji Oct 6, 2025
137b829
test: add tests for Koshien#move_to method
takaokouji Oct 6, 2025
6670793
test: add tests for Koshien#set_dynamite method
takaokouji Oct 6, 2025
4c23f8e
test: add tests for Koshien#set_bomb method
takaokouji Oct 6, 2025
cdefeeb
test: add tests for Koshien#turn_over method
takaokouji Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 1 addition & 106 deletions lib/smalruby3/koshien.rb
Original file line number Diff line number Diff line change
@@ -1,112 +1,7 @@
require "singleton"
require "json"
require "time"

# ダイクストラ法により最短経路を求める
module DijkstraSearch
# 点
# 各点は"m0_0"のような形式のID文字列をもつ
class Node
attr_accessor :id, :edges, :cost, :done, :from
def initialize(id, edges = [], cost = nil, done = false)
@id, @edges, @cost, @done = id, edges, cost, done
end
end

# 辺
# Note: Edgeのインスタンスは必ずNodeに紐付いているため、片方の点ID(nid)しか持っていない
class Edge
attr_reader :cost, :nid
def initialize(cost, nid)
@cost, @nid = cost, nid
end
end

# グラフ
class Graph
# 新しいグラフをつくる
# data : 点のIDから、辺の一覧へのハッシュ
# 辺は[cost, nid]という形式
def initialize(data)
@nodes =
data.map do |id, edges|
edges.map! { |edge| Edge.new(*edge) }
Node.new(id, edges)
end
end

# 二点間の最短経路をNodeの一覧で返す(終点から始点へという順序なので注意)
# sid : 始点のID(例:"m0_0")
# gid : 終点のID
def route(sid, gid)
dijkstra(sid)
base = @nodes.find { |node| node.id == gid }

# Check if destination is reachable (cost should not be nil)
return [] if base.nil? || base.cost.nil?

@res = [base]
while base.from && (base = @nodes.find { |node| node.id == base.from })
@res << base
end
@res
end

# 二点間の最短経路を座標の配列で返す
# sid : 始点のID
# gid : 終点のID
def get_route(sid, gid)
result = route(sid, gid)
if result.empty?
# When destination is unreachable, return only starting position
sid =~ /\Am(\d+)_(\d+)\z/
return [[$1.to_i, $2.to_i]]
end

result.reverse.map { |node|
node.id =~ /\Am(\d+)_(\d+)\z/
[$1.to_i, $2.to_i]
}
end

# sidを始点としたときの、nidまでの最小コストを返す
def cost(nid, sid)
dijkstra(sid)
@nodes.find { |node| node.id == nid }.cost
end

private

# ある点からの最短経路を(破壊的に)設定する
# Nodeのcost(最小コスト)とfrom(直前の点)が更新される
# sid : 始点のID
def dijkstra(sid)
@nodes.each do |node|
node.cost = (node.id == sid) ? 0 : nil
node.done = false
node.from = nil
end
loop do
done_node = nil
@nodes.each do |node|
next if node.done || node.cost.nil?
done_node = node if done_node.nil? || node.cost < done_node.cost
end
break unless done_node
done_node.done = true
done_node.edges.each do |edge|
to = @nodes.find { |node| node.id == edge.nid }
cost = done_node.cost + edge.cost
from = done_node.id
if to.cost.nil? || cost < to.cost
to.cost = cost
to.from = from
end
end
end
end
end
end
require_relative "../dijkstra_search"

module Smalruby3
# スモウルビー甲子園のAIを作るためのクラス
Expand Down
2 changes: 2 additions & 0 deletions lib/smalruby3/list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def to_array_index(list_index:)

# - 0以上は1を足す
# - -1以下はそのまま
# - nilはnilのまま
def to_list_index(array_index:)
return nil if array_index.nil?
return array_index + 1 if array_index >= 0

array_index
Expand Down
232 changes: 232 additions & 0 deletions spec/lib/dijkstra_search_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe DijkstraSearch do
describe DijkstraSearch::Node do
describe "#initialize" do
it "creates a node with id and edges" do
edges = [DijkstraSearch::Edge.new(1, "m0_1")]
node = DijkstraSearch::Node.new("m0_0", edges)

expect(node.id).to eq("m0_0")
expect(node.edges).to eq(edges)
expect(node.cost).to be_nil
expect(node.done).to be false
end

it "creates a node with cost and done status" do
node = DijkstraSearch::Node.new("m0_0", [], 10, true)

expect(node.id).to eq("m0_0")
expect(node.cost).to eq(10)
expect(node.done).to be true
end
end
end

describe DijkstraSearch::Edge do
describe "#initialize" do
it "creates an edge with cost and node id" do
edge = DijkstraSearch::Edge.new(5, "m0_1")

expect(edge.cost).to eq(5)
expect(edge.nid).to eq("m0_1")
end
end
end

describe DijkstraSearch::Graph do
let(:simple_graph_data) do
{
"m0_0" => [[1, "m0_1"], [2, "m1_0"]],
"m0_1" => [[1, "m0_0"], [1, "m0_2"]],
"m0_2" => [[1, "m0_1"]],
"m1_0" => [[2, "m0_0"]]
}
end

let(:graph) { DijkstraSearch::Graph.new(simple_graph_data) }

describe "#initialize" do
it "creates a graph from data hash" do
expect(graph).to be_a(DijkstraSearch::Graph)
end

it "creates nodes with edges" do
nodes = graph.instance_variable_get(:@nodes)
expect(nodes.length).to eq(4)
expect(nodes.map(&:id)).to contain_exactly("m0_0", "m0_1", "m0_2", "m1_0")
end
end

describe "#route" do
context "when path exists" do
it "finds shortest route from start to goal" do
route = graph.route("m0_0", "m0_2")

expect(route).to be_an(Array)
expect(route.first.id).to eq("m0_2") # First is goal (reverse order)
expect(route.last.id).to eq("m0_0") # Last is start
end

it "returns nodes in reverse order (goal to start)" do
route = graph.route("m0_0", "m0_2")

# Route should be: m0_2, m0_1, m0_0 (goal to start)
expect(route.map(&:id)).to eq(["m0_2", "m0_1", "m0_0"])
end
end

context "when path does not exist" do
it "returns single node for unreachable destination" do
isolated_graph_data = {
"m3_3" => [[1, "m3_4"]],
"m3_4" => [[1, "m3_3"]],
"m5_5" => [] # Isolated node
}
isolated_graph = DijkstraSearch::Graph.new(isolated_graph_data)

route = isolated_graph.route("m3_3", "m5_5")

# When destination exists but is unreachable, returns just the destination node
expect(route.length).to be >= 0
if route.length > 0
expect(route.first.id).to eq("m5_5")
expect(route.first.from).to be_nil
end
end

it "returns empty array for non-existent destination" do
route = graph.route("m0_0", "non_existent")

expect(route).to eq([])
end
end

context "when start equals goal" do
it "returns single node route" do
route = graph.route("m0_0", "m0_0")

expect(route.length).to eq(1)
expect(route.first.id).to eq("m0_0")
end
end
end

describe "#dijkstra (via route)" do
it "calculates shortest path costs correctly" do
graph.route("m0_0", "m0_2")
nodes = graph.instance_variable_get(:@nodes)

# m0_0 (start) -> cost 0
# m0_1 -> cost 1 (from m0_0)
# m0_2 -> cost 2 (from m0_1)
# m1_0 -> cost 2 (from m0_0)
node_0_0 = nodes.find { |n| n.id == "m0_0" }
node_0_1 = nodes.find { |n| n.id == "m0_1" }
node_0_2 = nodes.find { |n| n.id == "m0_2" }

expect(node_0_0.cost).to eq(0)
expect(node_0_1.cost).to eq(1)
expect(node_0_2.cost).to eq(2)
end
end

describe "complex graph scenarios" do
let(:complex_graph_data) do
{
"m0_0" => [[1, "m1_0"], [4, "m0_1"]],
"m1_0" => [[1, "m2_0"], [2, "m1_1"]],
"m2_0" => [[1, "m2_1"]],
"m0_1" => [[1, "m1_1"]],
"m1_1" => [[1, "m2_1"]],
"m2_1" => []
}
end

let(:complex_graph) { DijkstraSearch::Graph.new(complex_graph_data) }

it "finds shortest path among multiple routes" do
route = complex_graph.route("m0_0", "m2_1")

# Shortest path: m0_0 -> m1_0 -> m2_0 -> m2_1 (cost: 3)
# Alternative: m0_0 -> m0_1 -> m1_1 -> m2_1 (cost: 6)
expect(route.map(&:id)).to eq(["m2_1", "m2_0", "m1_0", "m0_0"])
end
end

describe "#get_route" do
context "when path exists" do
it "returns route as coordinates array in normal order (start to goal)" do
route_coords = graph.get_route("m0_0", "m0_2")

expect(route_coords).to be_an(Array)
expect(route_coords.first).to eq([0, 0]) # Start
expect(route_coords.last).to eq([0, 2]) # Goal
end

it "converts node IDs to coordinate arrays" do
route_coords = graph.get_route("m0_0", "m0_2")

# Expected: [[0, 0], [0, 1], [0, 2]]
expect(route_coords.length).to eq(3)
expect(route_coords[0]).to eq([0, 0])
expect(route_coords[1]).to eq([0, 1])
expect(route_coords[2]).to eq([0, 2])
end
end

context "when path does not exist" do
it "returns only starting position for unreachable destination" do
isolated_graph_data = {
"m0_0" => [[1, "m0_1"]],
"m0_1" => [[1, "m0_0"]],
"m0_2" => [] # Isolated node
}
isolated_graph = DijkstraSearch::Graph.new(isolated_graph_data)

route_coords = isolated_graph.get_route("m0_0", "m0_2")

expect(route_coords).to eq([[0, 0]])
end

it "returns only starting position for non-existent destination" do
route_coords = graph.get_route("m0_0", "m9_9")

expect(route_coords).to eq([[0, 0]])
end
end

context "when start equals goal" do
it "returns single coordinate" do
route_coords = graph.get_route("m0_0", "m0_0")

expect(route_coords).to eq([[0, 0]])
end
end
end

describe "#cost" do
it "returns minimum cost from start to destination" do
cost = graph.cost("m0_2", "m0_0")

# Path: m0_0 -> m0_1 -> m0_2 with cost 1 + 1 = 2
expect(cost).to eq(2)
end

it "returns 0 for start node" do
cost = graph.cost("m0_0", "m0_0")

expect(cost).to eq(0)
end

it "handles different start nodes" do
cost = graph.cost("m0_0", "m0_2")

# Reverse path: m0_2 -> m0_1 -> m0_0 with cost 1 + 1 = 2
expect(cost).to eq(2)
end
end
end
end
34 changes: 34 additions & 0 deletions spec/lib/smalruby3/ignore_method_missing_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Smalruby3::IgnoreMethodMissing do
let(:instance) { described_class.new }

describe "#method_missing" do
it "returns a new instance of the class" do
result = instance.some_undefined_method

expect(result).to be_a(described_class)
expect(result).not_to eq(instance)
end

it "handles method calls with arguments" do
result = instance.another_method(1, 2, 3)

expect(result).to be_a(described_class)
end

it "warns about the missing method" do
expect { instance.missing_method }.to output(/no method error/).to_stderr
end
end

describe "#respond_to_missing?" do
it "delegates to super" do
# respond_to_missing? should return false for non-existent methods
# when delegating to super (default Object behavior)
expect(instance.respond_to?(:some_nonexistent_method, true)).to be false
end
end
end
Loading