From 4d1e8d7a64bf9740048dfbcde064ca1f955b1e85 Mon Sep 17 00:00:00 2001 From: Yury Shchyhlinski Date: Thu, 21 Feb 2019 03:27:12 +0300 Subject: [PATCH] Implement base logic * Implement "POST /play" API endpoint * Implement classes to store and compare choices. * Implement classes to perform robots throw. --- .gitignore | 2 + .ruby-version | 1 + Gemfile | 9 ++++ Gemfile.lock | 85 +++++++++++++++++++++++++++++++++++++ README.md | 71 ++++++++++++++++++++++++++++++- config.ru | 3 ++ init.rb | 19 +++++++++ lib/api.rb | 28 ++++++++++++ lib/choice.rb | 12 ++++++ lib/choice/base.rb | 13 ++++++ lib/choice/beatable.rb | 19 +++++++++ lib/choice/hammer.rb | 7 +++ lib/choice/paper.rb | 7 +++ lib/choice/rock.rb | 7 +++ lib/choice/scissors.rb | 7 +++ lib/choice/unknown.rb | 8 ++++ lib/robot_throw.rb | 16 +++++++ lib/robot_throw/base.rb | 8 ++++ lib/robot_throw/curb_api.rb | 27 ++++++++++++ lib/robot_throw/random.rb | 8 ++++ lib/web.rb | 5 +++ 21 files changed, 360 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 config.ru create mode 100644 init.rb create mode 100644 lib/api.rb create mode 100644 lib/choice.rb create mode 100644 lib/choice/base.rb create mode 100644 lib/choice/beatable.rb create mode 100644 lib/choice/hammer.rb create mode 100644 lib/choice/paper.rb create mode 100644 lib/choice/rock.rb create mode 100644 lib/choice/scissors.rb create mode 100644 lib/choice/unknown.rb create mode 100644 lib/robot_throw.rb create mode 100644 lib/robot_throw/base.rb create mode 100644 lib/robot_throw/curb_api.rb create mode 100644 lib/robot_throw/random.rb create mode 100644 lib/web.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1b6ee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +/.bundle diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..fad066f --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.5.0 \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..fc2de78 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '2.5.0' + +gem 'sinatra', '~> 2.0.4' +gem 'grape', '~> 1.1.0' +gem 'puma', '~> 3.11' +gem 'http', '~> 3.3.0' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..8c4220c --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,85 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (5.2.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + builder (3.2.3) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + concurrent-ruby (1.1.4) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + domain_name (0.5.20180417) + unf (>= 0.0.5, < 1.0.0) + equalizer (0.0.11) + grape (1.1.0) + activesupport + builder + mustermann-grape (~> 1.0.0) + rack (>= 1.3.0) + rack-accept + virtus (>= 1.0.0) + http (3.3.0) + addressable (~> 2.3) + http-cookie (~> 1.0) + http-form_data (~> 2.0) + http_parser.rb (~> 0.6.0) + http-cookie (1.0.3) + domain_name (~> 0.5) + http-form_data (2.1.1) + http_parser.rb (0.6.0) + i18n (1.5.3) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + minitest (5.11.3) + mustermann (1.0.3) + mustermann-grape (1.0.0) + mustermann (~> 1.0.0) + public_suffix (3.0.3) + puma (3.12.0) + rack (2.0.6) + rack-accept (0.4.5) + rack (>= 0.4) + rack-protection (2.0.4) + rack + sinatra (2.0.4) + mustermann (~> 1.0) + rack (~> 2.0) + rack-protection (= 2.0.4) + tilt (~> 2.0) + thread_safe (0.3.6) + tilt (2.0.9) + tzinfo (1.2.5) + thread_safe (~> 0.1) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.5) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) + +PLATFORMS + ruby + +DEPENDENCIES + grape (~> 1.1.0) + http (~> 3.3.0) + puma (~> 3.11) + sinatra (~> 2.0.4) + +RUBY VERSION + ruby 2.5.0p0 + +BUNDLED WITH + 1.16.5 diff --git a/README.md b/README.md index a38ea0d..15cf2e4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,69 @@ -# rps -Rock-Paper-Scissors +# How to install + +1. Pull the source code from repository. +2. Change current directory to project's root. +3. Install gems by executing command `bundle install`. +4. Please readme `How to test` section below to try it. + +# How to test + +NOTE that UI isn't implemented. +That's why it's possible to test only API. + +**How to test:** + +1. From source directory run application server with command `puma -p 3000`. +2. You can test `POST /play` endpoint with single parameter `choice` which accepts any string. + Please see the CURL examples below. + +# CURL examples + +Request: +```bash +curl -d "choice=rock" -X POST http://localhost:3000/play +``` + +Response: +```json +{ + "user_choice":"rock", + "robot_choice":"paper", + "result":{ + "winner":"robot", + "message":"paper beats rock" + } +} +``` + +Request: +```bash +curl -d "choice=hammer" -X POST http://localhost:3000/play +``` + +Response: +```json +{ + "user_choice":"hammer", + "robot_choice":"rock", + "result":{ + "winner":"user", + "message":"hammer beats rock" + } +} +``` + +Request: +```bash +curl -d "choice=scissors" -X POST http://localhost:3000/play +``` + +```json +{ + "user_choice":"scissors", + "robot_choice":"scissors", + "result":{ + "winner":"tie", + "message":"in this game scissors is the same as scissors" + } +} +``` \ No newline at end of file diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..7b0256b --- /dev/null +++ b/config.ru @@ -0,0 +1,3 @@ +require_relative './init' + +run Rack::Cascade.new [Web, Api] diff --git a/init.rb b/init.rb new file mode 100644 index 0000000..f0bc25a --- /dev/null +++ b/init.rb @@ -0,0 +1,19 @@ +require 'bundler/setup' +Bundler.require(:default) + +require_relative 'lib/choice' +require_relative 'lib/choice/beatable' +require_relative 'lib/choice/base' +require_relative 'lib/choice/rock' +require_relative 'lib/choice/paper' +require_relative 'lib/choice/scissors' +require_relative 'lib/choice/hammer' +require_relative 'lib/choice/unknown' + +require_relative 'lib/robot_throw/base' +require_relative 'lib/robot_throw/curb_api' +require_relative 'lib/robot_throw/random' +require_relative 'lib/robot_throw' + +require_relative 'lib/web' +require_relative 'lib/api' diff --git a/lib/api.rb b/lib/api.rb new file mode 100644 index 0000000..ce85542 --- /dev/null +++ b/lib/api.rb @@ -0,0 +1,28 @@ +class Api < Grape::API + format :json + + params do + requires :choice, type: Choice::Base, coerce_with: ->(value) { Choice.by(value).new(value) } + end + + post :play do + user_choice = params[:choice] + robot_choice = RobotThrow.throw + + if user_choice.class.beats?(robot_choice.class) + result = { winner: 'user', message: "#{user_choice} beats #{robot_choice}" } + elsif user_choice.class.beaten?(robot_choice.class) + result = { winner: 'robot', message: "#{robot_choice} beats #{user_choice}" } + elsif user_choice.class.tie?(robot_choice.class) + result = { winner: 'tie', message: "in this game #{robot_choice} is the same as #{user_choice}" } + else + raise StandardError, 'something went wrong, please let developers know :)' + end + + present( + user_choice: user_choice.to_s, + robot_choice: robot_choice.to_s, + result: result + ) + end +end diff --git a/lib/choice.rb b/lib/choice.rb new file mode 100644 index 0000000..84de98e --- /dev/null +++ b/lib/choice.rb @@ -0,0 +1,12 @@ +module Choice + + def self.by(name) + class_name = name.to_s.downcase.strip.capitalize + + if const_defined?(class_name) + const_get(class_name, false) + else + Unknown + end + end +end diff --git a/lib/choice/base.rb b/lib/choice/base.rb new file mode 100644 index 0000000..49c96b4 --- /dev/null +++ b/lib/choice/base.rb @@ -0,0 +1,13 @@ +module Choice + class Base + extend Beatable + + def initialize(original_name = nil) + @original_name = original_name || self.class.name.split('::').last.downcase + end + + def to_s + @original_name.to_s + end + end +end diff --git a/lib/choice/beatable.rb b/lib/choice/beatable.rb new file mode 100644 index 0000000..21d8f0c --- /dev/null +++ b/lib/choice/beatable.rb @@ -0,0 +1,19 @@ +module Choice + module Beatable + def beats + raise NotImplementedError + end + + def beats?(choice) + beats.include?(choice) + end + + def beaten?(choice) + choice.beats?(self) + end + + def tie?(choice) + !beats?(choice) && !beaten?(choice) + end + end +end \ No newline at end of file diff --git a/lib/choice/hammer.rb b/lib/choice/hammer.rb new file mode 100644 index 0000000..5a96322 --- /dev/null +++ b/lib/choice/hammer.rb @@ -0,0 +1,7 @@ +module Choice + class Hammer < Base + def self.beats + [Paper, Rock, Scissors] + end + end +end diff --git a/lib/choice/paper.rb b/lib/choice/paper.rb new file mode 100644 index 0000000..ee92376 --- /dev/null +++ b/lib/choice/paper.rb @@ -0,0 +1,7 @@ +module Choice + class Paper < Base + def self.beats + [Rock] + end + end +end diff --git a/lib/choice/rock.rb b/lib/choice/rock.rb new file mode 100644 index 0000000..2f6a412 --- /dev/null +++ b/lib/choice/rock.rb @@ -0,0 +1,7 @@ +module Choice + class Rock < Base + def self.beats + [Scissors] + end + end +end diff --git a/lib/choice/scissors.rb b/lib/choice/scissors.rb new file mode 100644 index 0000000..e53476c --- /dev/null +++ b/lib/choice/scissors.rb @@ -0,0 +1,7 @@ +module Choice + class Scissors < Base + def self.beats + [Paper] + end + end +end \ No newline at end of file diff --git a/lib/choice/unknown.rb b/lib/choice/unknown.rb new file mode 100644 index 0000000..42e5c09 --- /dev/null +++ b/lib/choice/unknown.rb @@ -0,0 +1,8 @@ +module Choice + class Unknown < Base + + def self.beats + [] + end + end +end diff --git a/lib/robot_throw.rb b/lib/robot_throw.rb new file mode 100644 index 0000000..1925d07 --- /dev/null +++ b/lib/robot_throw.rb @@ -0,0 +1,16 @@ +module RobotThrow + + module Adapters + CURB_API = CurbApi + RANDOM = Random + + ALL = [CURB_API, RANDOM].freeze + end + + def self.throw + Adapters::ALL.find do |adapter| + choice = adapter.new.throw + return choice if choice + end + end +end diff --git a/lib/robot_throw/base.rb b/lib/robot_throw/base.rb new file mode 100644 index 0000000..168ed89 --- /dev/null +++ b/lib/robot_throw/base.rb @@ -0,0 +1,8 @@ +module RobotThrow + class Base + + def throw + raise NotImplementedError + end + end +end diff --git a/lib/robot_throw/curb_api.rb b/lib/robot_throw/curb_api.rb new file mode 100644 index 0000000..9f18363 --- /dev/null +++ b/lib/robot_throw/curb_api.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module RobotThrow + class CurbApi < Base + ENDPOINT = 'https://5eddt4q9dk.execute-api.us-east-1.amazonaws.com/rps-stage/throw' + TIMEOUT = 3 + + def throw + response = HTTP.timeout(connect: TIMEOUT, read: TIMEOUT).get(ENDPOINT) + return unless success?(response) + + curb_choice = response.parse.fetch('body') + curb_choice = curb_choice.delete('"') + + Choice.by(curb_choice).new(curb_choice) + + rescue StandardError + nil + end + + private + + def success?(response) + response.status.success? && response.parse['statusCode'] == 200 + end + end +end diff --git a/lib/robot_throw/random.rb b/lib/robot_throw/random.rb new file mode 100644 index 0000000..25210fb --- /dev/null +++ b/lib/robot_throw/random.rb @@ -0,0 +1,8 @@ +module RobotThrow + class Random < Base + + def throw + [Choice::Rock, Choice::Paper, Choice::Scissors].sample.new + end + end +end diff --git a/lib/web.rb b/lib/web.rb new file mode 100644 index 0000000..ef3136f --- /dev/null +++ b/lib/web.rb @@ -0,0 +1,5 @@ +class Web < Sinatra::Base + get '/' do + 'Please read README.md to see instructions' + end +end