From 39c0b034ca5ed03b750d2c7a1e4346cf28d19288 Mon Sep 17 00:00:00 2001 From: Fernando Valverde Date: Thu, 28 Sep 2023 18:00:18 -0600 Subject: [PATCH] Improve strategy choice based on path (#27) --- spec/server_spec.cr | 50 ++++++++++++++++++++++++++----------- src/app.cr | 56 ++++++++++++++++++++++-------------------- src/strategy/base.cr | 33 +++++++++++++++++++++---- src/strategy/random.cr | 2 +- 4 files changed, 95 insertions(+), 46 deletions(-) diff --git a/spec/server_spec.cr b/spec/server_spec.cr index 621d68a..c7c72f0 100644 --- a/spec/server_spec.cr +++ b/spec/server_spec.cr @@ -1,26 +1,48 @@ require "./spec_helper" -describe "Crystal Snake Server" do - it "returns metadata on root path" do - get "/" - response.status_code.should eq 200 +request_headers = HTTP::Headers{"Content-Type" => "application/json"} +SUPPORTED_STRATEGIES = [ + "random", + "random_valid", + "blast_random_valid", + "chase_closest_food", + "chase_random_food", + "cautious_carol" +] + +describe "Crystal Snake Battlesnake endpoints for all supported strategies" do + it "responds with metadata for all supported strategies" do + SUPPORTED_STRATEGIES.each do |strategy_name| + get "/#{strategy_name}" + response.status_code.should eq 200 + res = Hash(String, String).from_json(response.body) + res.keys.should eq(["apiversion", "author", "color", "head", "tail", "version"]) + end end it "returns 200 on /start" do - payload = File.read("./spec/fixtures/start.json") - post "/start", body: payload, headers: HTTP::Headers{"Content-Type" => "application/json"} - response.status_code.should eq 200 + SUPPORTED_STRATEGIES.each do |strategy_name| + payload = File.read("./spec/fixtures/start.json") + post "/#{strategy_name}/start", body: payload, headers: request_headers + response.status_code.should eq 200 + end end - it "returns 200 on /move" do - payload = File.read("./spec/fixtures/move.json") - post "/move", body: payload, headers: HTTP::Headers{"Content-Type" => "application/json"} - response.status_code.should eq 200 + it "returns 200 and valid move on /move" do + SUPPORTED_STRATEGIES.each do |strategy_name| + payload = File.read("./spec/fixtures/move.json") + post "/#{strategy_name}/move", body: payload, headers: request_headers + response.status_code.should eq 200 + selected_move = Hash(String, String).from_json(response.body)["move"] + (Strategy::VALID_MOVES.includes?(selected_move)).should be_true + end end it "returns 200 on /end" do - payload = File.read("./spec/fixtures/end.json") - post "/end", body: payload, headers: HTTP::Headers{"Content-Type" => "application/json"} - response.status_code.should eq 200 + SUPPORTED_STRATEGIES.each do |strategy_name| + payload = File.read("./spec/fixtures/end.json") + post "/#{strategy_name}/end", body: payload, headers: HTTP::Headers{"Content-Type" => "application/json"} + response.status_code.should eq 200 + end end end diff --git a/src/app.cr b/src/app.cr index c54124c..e521463 100644 --- a/src/app.cr +++ b/src/app.cr @@ -9,9 +9,17 @@ require "../config/**" require "./models/**" require "./jobs/**" +add_context_storage_type(Strategy::Base) persist_to_db = ENV["DISABLE_DB_PERSIST"]?.nil? -selected_strategy = ENV["STRATEGY"] ||= "RandomValid" -Log.info { "Selected strategy: #{selected_strategy}" } +not_found_response = " +

+

+ Strategy Not Found. +

+ Read usage details +
+

+" macro persist_turn! PersistTurnJob.new( @@ -25,10 +33,23 @@ def truncate_uuid(str) end before_all do |env| - env.response.content_type = "application/json" unless env.request.path =~ /\/games?/ + next if env.request.path =~ /\/games?/ + + env.response.content_type = "application/json" + next if env.params.json.empty? + + context = BattleSnake::Context.from_json(env.params.json.to_json) + strategy = Strategy.build(env.params.url["strategy"], context) + halt env, status_code: 404, response: not_found_response if strategy.nil? + env.set("strategy", strategy) +end + +get "/strategy_not_found" do |env| + halt env, status_code: 404, response: not_found_response end -get "/" do +# Battlesnake API Endpoints +get "/:strategy" do |env| { "apiversion": "1", "author": "fdocr", @@ -39,39 +60,22 @@ get "/" do }.to_json end -post "/start" do |env| - context = BattleSnake::Context.from_json(env.params.json.to_json) +post "/:strategy/start" do |env| persist_turn! end -post "/move" do |env| - context = BattleSnake::Context.from_json(env.params.json.to_json) +post "/:strategy/move" do |env| persist_turn! - - case selected_strategy - when "RandomValid" - move = Strategy::RandomValid.new(context).move - when "BlastRandomValid" - move = Strategy::BlastRandomValid.new(context).move - when "ChaseClosestFood" - move = Strategy::ChaseClosestFood.new(context).move - when "ChaseRandomFood" - move = Strategy::ChaseRandomFood.new(context).move - when "CautiousCarol" - move = Strategy::CautiousCarol.new(context).move - else - move = Strategy::RandomValid.new(context).move - end - + move = env.get("strategy").as(Strategy::Base).move res = { "move": move, "shout": "Moving #{move}!" } res.to_json end -post "/end" do |env| - context = BattleSnake::Context.from_json(env.params.json.to_json) +post "/:strategy/end" do |env| persist_turn! end +# DB-persisted games get "/games" do |env| offset = (env.params.query["page"]? || 0).to_i * 50 count = Turn.where { _path == "/end" }.count diff --git a/src/strategy/base.cr b/src/strategy/base.cr index e542b6a..a62e0c4 100644 --- a/src/strategy/base.cr +++ b/src/strategy/base.cr @@ -1,11 +1,34 @@ # Abstract class of all strategies. They're all initialized with a *@context* # and their entrypoint is the `#move` method -abstract class Strategy::Base - def initialize(@context : BattleSnake::Context) +module Strategy + VALID_MOVES = ["up", "left", "down", "right"] + + def self.build(name, context) + case name + when "random" + Strategy::Random.new(context) + when "random_valid" + Strategy::RandomValid.new(context) + when "blast_random_valid" + Strategy::BlastRandomValid.new(context) + when "chase_closest_food" + Strategy::ChaseClosestFood.new(context) + when "chase_random_food" + Strategy::ChaseClosestFood.new(context) + when "cautious_carol" + Strategy::CautiousCarol.new(context) + else + nil + end end - # Returns the move (direction) to chose based on the *@context* - def move - "up" + abstract class Base + def initialize(@context : BattleSnake::Context) + end + + # Returns the move (direction) to chose based on the *@context* + def move + "up" + end end end \ No newline at end of file diff --git a/src/strategy/random.cr b/src/strategy/random.cr index 6064ed9..dab1e30 100644 --- a/src/strategy/random.cr +++ b/src/strategy/random.cr @@ -1,6 +1,6 @@ # A strategy that chooses a random direction to move without any considerations class Strategy::Random < Strategy::Base def move - ["up", "left", "down", "right"].sample + VALID_MOVES.sample end end \ No newline at end of file