This section is included to assist the students to perform the activities present in the book. It includes detailed steps that are to be performed by the students to complete and achieve the objectives of the book.
Solution
puts "Enter your first name: "
first_name = gets.chomp
puts "Enter your last name: "
last_name = gets.chomp
puts "#{first_name}#{last_name}@rubyprogram.com"
#Output :
Enter your first name:
akshat
Enter your last name:
paul
Expected output:
Solution
print "Enter the radius for the circular candy: "
radius = gets.to_f
perimeter = 2 * 3.141592653 * radius
area = 3.141592653 * radius * radius
puts "The perimeter of the candy is #{perimeter}."
puts "The area of the candy is #{area}."
Expected output:
Solution
rand 2
def roll
rand(6) + 1
end
puts roll
def roll(sides)
rand(sides) + 1
end
puts roll(6)
def roll(sides, number=1)
roll_array = []
number.times do
roll_value = rand(sides) + 1
roll_array << roll_value
end
total = 0
roll_array.each do |roll|
new_total = total + roll
total = new_total
end
total
end
puts "Rolling a 5 sided die!"
puts roll(5)
puts "Rolling two 6 sided dice!"
puts roll(6, 2)
Let's now understand the preceding code. We added an argument, number, which has a default value of 1, so that in case we don't pass anything, the program continues to work. We then created an empty array to keep the die we are going to roll, called roll_array. The times method of the number variable will create a for loop for us, iterating over the body of the loop a number of times. By default, one iteration will happen. Every iteration will add a new value to our roll_array method based on the number of sides our die has. The last part of our function will return the sum of the rolled values with our dice, simple as that.
Solution
play_choice = 'y'
while play_choice == 'y'
puts "Welcome to HiLow - Shall we play"
play_choice = gets.chomp.downcase
if play_choice == 'y'
play_game
end
end
puts "Thanks for playing!"
def try_guess(magic_number, guess)
if guess == magic_number
puts "You guess correctly!"
return true
elsif guess < magic_number
puts "Guess higher"
return false
else
puts "Guess lower"
return false
end
end
Activity3.01.rb
1 def play_game
2 print "I'm going to pick a random number that you will have to guess. Please enter the maximum number for the guessing range."
3 range = gets.chomp.to_i
4 magic_number = rand(range)
5 until try_guess(magic_number) do
6 end
7 end
8
9 def try_guess(magic_number)
10 print "What's your guess? "
11 guess = gets.chomp.to_i
12 if guess == magic_number
The game will now look as shown in the following figure:
Solution
def generate_deck
cards = (1..13)
suits = ["Diamonds", "Clubs", "Spades", "Hearts"]
deck = []
suits.each do |suit|
cards.each do |card|
deck << [card, suit]
end
end
return deck
end
def shuffle_deck(deck)
shuffled_deck = []
while(deck.length > 0) do
random_card_index = rand(deck.length)
shuffled_deck << deck.delete_at(random_card_index)
end
return shuffled_deck
end
def inspect_cards(cards, format: :short)
hand = cards.map{|c| card_label(c, format: format)}.join(",")
total = calculate_total(cards)
"#{hand} (#{total})"
end
def card_label(card, format: :short)
card_labels = {1 => "Ace", 11 => "Jack", 12 => "Queen", 13 => "King"}
card_suit_labels = {"Diamonds" => "♦", "Clubs" => "♣", "Spades" => "♠", "Hearts" => "♥"}
card_index, card_suit = card
label = card_labels[card_index] || card_index
icon = card_suit_labels[card_suit]
if format == :short
"#{label}#{icon}"
else
"#{label} of #{card_suit}"
end
end
Activity4.01.rb
75 def card_value(cards, card)
76 case card
77 # face cards are 10
78 when 11..13
79 10
80 when 1 # Ace can be 1 or 11 depending on the rest of the cards
81 # simple algorithm for determining what Ace should count as
82 # get total value of all non-ace cards
83 non_ace_cards = cards.reject{|c| c[0] == 1}
84 non_ace_card_total = calculate_total(non_ace_cards)
85 # If the Ace as 11 busts us, count it as a 1
86 if (non_ace_card_total + 11) > 21
87 1
88 # If the Ace as an 11, gets us to 21, count it as an 11
89 elsif (non_ace_card_total + 11) == 21
90 11
A way for the dealer to determine whether it should hit or stay is for the dealer to follow a fixed set of rules. If the total of the current hand is less than 17, it must hit. If the dealer's hand totals 17 or higher, it must stay:
Activity4.01.rb
36 def play(deck)
37 player_hand = deal_cards(deck, 2)
38 dealer_hand = deal_cards(deck, 2)
39 puts "Player has: #{inspect_cards(player_hand)}"
40 puts "Dealer has: #{inspect_cards([dealer_hand[0]])}, <other card hidden>"
41 choice = nil
42 while choice != 'stay' && calculate_total(player_hand) <= 21 do
43 print "Do you want to hit or stay?"
44 choice = gets.chomp
45 if choice == 'hit'
46 player_hand += deal_cards(deck, 1)
47 end
48 print "Your cards are now: #{inspect_cards(player_hand)} "
49 end
50 while((dealer_total = calculate_total(dealer_hand)) <= 21 && (dealer_total < 17)) do
shuffled_deck = shuffle_deck(generate_deck)
choice = 'y'
while(shuffled_deck.length > 4 && choice.downcase != 'n') do # need at least 4 cards to play
puts "Deck has: #{shuffled_deck.length} cards left"
print "Do you want to play a hand?[Yn]"
choice = gets.chomp
if choice.downcase == 'y'
play(shuffled_deck)
end
end
# Tests
# puts calculate_total([[12, "Hearts"]]) # 10
# puts calculate_total([[1,"Spades"],[12, "Hearts"]]) #21
# puts calculate_total([[13,"Hearts"],[6, "Hearts"]]) # 16
Here is the expected output:
Solution
git clone https://github.com/PacktWorkshops/The-Ruby-Workshop/tree/master/Chapter05/Activity5.01/framework
voting_machine.rb
1 class VotingMachine
2 attr_reader :month, :year, :results
3
4 class InvalidVote < Exception; end
5
6 def initialize(month, year)
7 @month = month
8 @year = year
9 @results = {}
10 end
11
12 def record_vote(voter, votee)
13 raise InvalidVote unless valid_vote?(voter, votee)
14 results[votee] ||= 0
15 results[votee] += 1
16 end
class VoteController < Controller
attr_accessor :voter, :votee
def run
get_input :voter, "What's your name? "
get_input :votee, "Who do you want to vote for? "
self.voting_machine.record_vote(@voter, @votee)
log "Vote recorded!"
end
end
MENU_CHOICES = {
1 => {controller: :vote, label: "Place a vote for a colleague"},
2 => {controller: :exit, label: "Exit"}
}.freeze
class LeaderboardController < Controller
def run
sorted_results = voting_machine.sorted_results
sorted_results.each do |person, count|
log "#{person}: #{count}"
end
end
end
MENU_CHOICES = {
1 => {controller: :vote, label: "Place a vote for a colleague"},
2 => {controller: :leaderboard, label: "See current leaderboard"},
3 => {controller: :exit, label: "Exit"}
}.freeze
test_controller.rb
51 def test_menu_controller
52 stubbed_input_method = Proc.new do |variable_symbol, question|
53 instance_variable_set("@choice", $TEST_VARS[:choice])
54 end
55
56 stub_controller with: stubbed_input_method do
57 menu_controller = MenuController.new(nil)
58
59 $TEST_VARS = {choice: 1, display: ""}
60 assert_equal :vote, menu_controller.run
61 assert_match /Please enter your choice/, $TEST_VARS[:display]
62
63 $TEST_VARS = {choice: 2, display: ""}
64 assert_equal :leaderboard, menu_controller.run
65 assert_match /Please enter your choice/, $TEST_VARS[:display]
ruby test.rb
The output will be as follows:
application.rb
1 #!/usr/bin/env ruby
2
3 # require all files in models and controllers directory
4 require './model'
5 require './controller'
6 require 'byebug'
7
8 class InvalidChoiceException < Exception;end
9
10 # Create master class called Application
11 # which will be the core class that manages the loop
12 # around the voting machine
13 class Application
14 attr_reader :voting_machine
15
Expected Output:
Solution
require "minitest/autorun"
require 'minitest/stub_any_instance'
require_relative "../models/voting_machine"
class TestVotingMachine < Minitest::Test
def test_add_category
machine = VotingMachine.new(1, 1)
machine.add_category("TestCategory")
assert_equal machine.categories, ["TestCategory"]
machine.add_category("TestCategory2")
assert_equal machine.categories, ["TestCategory", "TestCategory2"]
machine.add_category("TestCategory")
assert_equal machine.categories, ["TestCategory", "TestCategory2"]
assert_equal machine.send(:valid_category?, "Invalid"), false
end
end
class VotingMachine
attr_reader :month, :year, :results, :categories
class InvalidCategory < Exception; end
class InvalidVote < Exception; end
def add_category(category)
@categories << category
@categories.uniq! # make sure no duplicates
end
private
def valid_category?(category)
categories.include?(category) ? true : false
end
end
The output would be as follows:
def test_add_vote
machine = VotingMachine.new(1, 1)
category1 = "TestCategory1"
category2 = "TestCategory2"
machine.add_category(category1)
machine.add_category(category2)
machine.record_vote(category1, "Bob", "Mary")
machine.record_vote(category1, "Suzie", "Mary")
machine.record_vote(category1, "Sam", "Bob")
machine.record_vote(category2, "Jackie", "Sam")
assert_equal machine.results.class, Hash
assert_equal machine.results.keys, [category1, category2]
assert_raises VotingMachine::InvalidCategory do
machine.record_vote("Invalid category", "Sue", "John")
end
end
class VotingMachine
attr_reader :month, :year, :results, :categories
class InvalidCategory < Exception; end
def record_vote(category, voter, votee)
raise InvalidCategory unless valid_category?(category)
raise InvalidVote unless valid_vote?(voter, votee)
results[category] ||= {}
results[category][votee] ||= 0
results[category][votee] += 1
end
end
def test_run_vote_controller
$TEST_VARS = {voter: "bob", votee: "mary", category: "TestCategory"}
stubbed_input_method = Proc.new do |variable_symbol, question|
instance_variable_set("@category", $TEST_VARS[:category])
instance_variable_set("@voter", $TEST_VARS[:voter])
instance_variable_set("@votee", $TEST_VARS[:votee])
end
# We have to stub the get_input method since this method grabs
# input from the terminal
stub_controller with: stubbed_input_method do
t = Time.now
machine = VotingMachine.new(t.month, t.year)
machine.add_category($TEST_VARS[:category])
Controller.run_controller(:vote, machine)
end
end
class VoteController < Controller
attr_accessor :voter, :votee, :category
def run
get_input :category, "Which category are you voting for: #{self.voting_machine.categories.join(", ")}"
get_input :voter, "What's your name? "
get_input :votee, "Who do you want to vote for? "
self.voting_machine.record_vote(@category, @voter, @votee)
log "Vote recorded!"
end
end
controller_logger.rb
1 module ControllerLogger
2 # If you aren't using a framework that gives you logging
3 # out of the box, its usually a good idea to implement a
4 # centralized logging method at the base class so you get
5 # consistent logging and do things like avoid logging output
6 # during tests
7
8 def self.prepended(parent_class)
9 parent_class.instance_eval do
10 def self.inherited(klass)
11 klass.send(:prepend, ControllerLogger)
12 end
13 end
14 end
15
The application would now be as follows:
Solution
category,votee,count
VoteCategoryA,Chris Jones,23
VoteCategoryA,Susie Bennet,29
VoteCategoryB,Allan Green,33
VoteCategoryB,Tony Bennet,23
require "minitest/autorun"
require 'minitest/stub_any_instance'
require_relative "../services/vote_importer"
class TestVoteImporter < Minitest::Test
def test_perform
# Import vote data from our tests/fixtures/votes.csv file
filepath = 'tests/fixtures/votes.csv'
results = VoteImporter.perform(filepath)
assert_equal results.length, 2
assert_equal results.map{|k,v| k}.include?("VoteCategoryA"), true
assert_equal results.map{|k,v| k}.include?("VoteCategoryB"), true
end
end
require "minitest/autorun"
require 'minitest/stub_any_instance'
require_relative "../services/vote_table"
class TestVoteTable < Minitest::Test
def test_perform
# Import votes data from our tests/fixtures/votes.csv file
filepath = 'tests/fixtures/votes.csv'
results = {"VoteCategoryA"=>[["Susie Bennet", 29]]},{"VoteCategoryB"=>[["Allan Green", 33]]}
tables = VoteTable.perform(results)
assert_equal tables.length, 2
tables.each do |table|
assert_equal table.class, Terminal::Table
assert_equal table.title.include?("VoteCategory"), true
end
end
end
require 'csv'
class VoteImporter
def initialize(filepath)
@filepath = filepath
end
def perform
results = {}
CSV.read(@filepath, headers: true).each do |row|
results[row["category"]] ||= {}
results[row["category"]][row["votee"]] = row["count"].to_i
end
results
end
def self.perform(*args)
new(*args).perform
end
end
require 'terminal-table'
class VoteTable
def initialize(sorted_votes)
@sorted_votes = sorted_votes
end
def perform
headings = ['Votee', 'Count']
tables = []
@sorted_votes.each do |votes|
tables << Terminal::Table.new(rows: votes.values[0], title: votes.keys[0], headings: headings)
end
tables
end
def self.perform(*args)
new(*args).perform
end
end
Now we just need to wire them up in our application.
MENU_CHOICES = {
1 => {controller: :vote, label: "Place a vote for a colleague"},
2 => {controller: :leaderboard, label: "See current leaderboard"},
3 => {controller: :category, label: "Add category"},
4 => {controller: :import, label: "Import Votes"},
5 => {controller: :exit, label: "Exit"}
}
def import_votes(filepath)
@results = VoteImporter.perform(filepath)
end
class ImportController < Controller
attr_accessor :filepath
def run
log "Import votes from an external CSV"
get_input :filepath, "Enter the filepath of the CSV file? "
self.voting_machine.import_votes(filepath)
log "Votes imported!"
end
end
sorted_results = voting_machine.sorted_results
tables = VoteTable.perform(sorted_results)
tables.each do |table|
log table
end
require "csv"
votes = [
["Category", "Votee", "Count"],
["Employee Of The Month", "Chris Jones", 23],
["Employee Of The Month", "Susie Bennet", 29],
["Employee Of The Month", "Bob Wing", 65],
["Employee Of The Month", "James King", 31],
["Best Christmas Sweater", "Allan Green", 33],
["Best Christmas Sweater", "Tony Bennet", 23],
["Best Christmas Sweater", "Bob Wing", 45],
["Best Christmas Sweater", "Jane Smith", 39],
]
CSV.open("votes.csv", "w") do |csv|
votes.each do |vote|
csv.puts [vote[0], vote[1], vote[2]]
end
end
votes.csv will now hold the following data:
category,votee,count
Employee Of The Month,Chris Jones,23
Employee Of The Month,Susie Bennet,29
Employee Of The Month,Bob Wing,65
Employee Of The Month,James King,31
Best Christmas Sweater,Allan Green,33
Best Christmas Sweater,Tony Bennet,23
Best Christmas Sweater,Bob Wing,45
Best Christmas Sweater,Jane Smith,39
If everything goes well, you should see an option in the menu to import votes:
Solution
tests/test_controller.rb
def test_controller_logger
t = Time.now
machine = VotingMachine.new(t.month, t.year)
controller = Controller.new(machine)
assert_instance_of(Logger, controller.instance_variable_get('@logger'))
end
controller_logger.rb
def initialize(voting_machine)
@logger = Logger.new('log.txt')
@logger.formatter = proc do |severity, datetime, progname, msg|
"#{severity} | #{datetime} | #{msg} "
end
super
end
controller_logger.rb
def log_to_file(msg, level = 'debug')
@logger.send(level, msg) unless ENV['TEST']
end
controller_logger.rb
def log(msg)
puts msg unless ENV['TEST']
log_to_file(msg)
end
controllers/vote_controller.rb
log_to_file("Vote recorded - Voter: #{@voter} Votee: #{@votee} Category: #{@category}", "info")
You should see output like the following:
Solution
def invoice_generator(p1,p2, &block)
yield calc_discount(p1,p2) if block_given?
end
The invoice_generator method has a yield keyword, which will only execute the block of code and pass the product prices if the first block of code is passed.
First, calc_discount is called and then a block code is executed, which, in turn, passes the customer details to the details method.
def calc_discount(p1,p2)
sum_of_products = p1.to_i+p2.to_i
final_amount = sum_of_products.to_i*0.80
puts "Price of Product 1: #{p1}"
puts "Price of Product 2: #{p2}"
puts "Final amount after 20% discount #{final_amount}"
end
def details(name, id)
puts "Customer name is #{name}"
puts "Customer id is #{id}"
end
puts "Enter your Customer Name"
cust_name = gets.chomp
puts "Enter your Customer ID"
cust_id = gets.chomp
puts "Enter Product 1 price"
product1 = gets.chomp
puts "Enter Product 2 price"
product2 = gets.chomp
invoice_generator(product1, product2) do
details(cust_name, cust_id)
end
ruby activity.rb
The output should be as follows:
Solution:
require 'httparty'
url = 'https://www.akshatpaul.com/ruby-fundamentals/list-all-buildings'
response = HTTParty.get(url)
puts response.parsed_response
$ruby get_request.rb
The output should be as follows:
You can implement the same code without a third-party gem dependency by using the in-built net/http library and the JSON library. To try this, refer to the code in the get_request_net.rb file:
require 'net/http'
require 'json'
url = 'https://www.akshatpaul.com/ruby-fundamentals/list-all-buildings'
uri = URI(url)
response = Net::HTTP.get(uri)
puts JSON.parse(response)
You will get the same result as we achieved with much leaner code.
require 'httparty'
url = "https://www.akshatpaul.com/ruby-fundamentals/buildings"
response = HTTParty.post(url, body: { property: {name: "Mr. Ruby Noobie", address: "Tokyo" }})
puts response.code
$ ruby post_request.rb
The output should be as follows:
If successful, we receive a proper status code and can then verify whether our record exists on the second URL provided.
Solution:
gem 'devise'
$ bundle install
$ rails generate devise:install
The output should be as follows:
This generator sets up all initial files required for devise. There are also some instructions related to customization, which is outside the scope of this activity.
$ rails generate devise User
The output is as follows:
Running via Spring preloader in process 15790
invoke active_record
create db/migrate/20190720201553_devise_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
insert app/models/user.rb
route devise_for :users
This devise generator has created an appropriate User model, along with migrations and routes for our application.
rails db:migrate
The output is as follows:
== 20190720201553 DeviseCreateUsers: migrating ================================
-- create_table(:users)
-> 0.0031s
-- add_index(:users, :email, {:unique=>true})
-> 0.0010s
-- add_index(:users, :reset_password_token, {:unique=>true})
-> 0.0010s
== 20190720201553 DeviseCreateUsers: migrated (0.0052s) =======================
$ rails routes
The output is as follows:
Prefix Verb URI Pattern Controller#Action
new_user_session GET /users/sign_in(.:format) devise/sessions#new
user_session POST /users/sign_in(.:format) devise/sessions#create
destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
new_user_password GET /users/password/new(.:format) devise/passwords#new
edit_user_password GET /users/password/edit(.:format) devise/passwords#edit
user_password PATCH /users/password(.:format) devise/passwords#update
PUT /users/password(.:format) devise/passwords#update
POST /users/password(.:format) devise/passwords#create
cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel
new_user_registration GET /users/sign_up(.:format) devise/registrations#new
edit_user_registration GET /users/edit(.:format) devise/registrations#edit
user_registration PATCH /users(.:format) devise/registrations#update
PUT /users(.:format) devise/registrations#update
DELETE /users(.:format) devise/registrations#destroy
POST /users(.:format) devise/registrations#create
root GET / home#index
reviews GET /reviews(.:format) reviews#index
POST /reviews(.:format) reviews#create
new_review GET /reviews/new(.:format) reviews#new
edit_review GET /reviews/:id/edit(.:format) reviews#edit
review GET /reviews/:id(.:format) reviews#show
PATCH /reviews/:id(.:format) reviews#update
PUT /reviews/:id(.:format) reviews#update
DELETE /reviews/:id(.:format) reviews#destroy
This is an out-of-the-box view that comes with the devise gem. For a simple application such as ours, this is quite good, but for anything more complex, we can always customize these views.
Note
You won't find this view in your application code; it's coming straight from the gem. To access and customize these default views, you can use rails generate devise:views, which will create these views within your application folders, where you can then modify them.
The Log in page would look as follows:
The blog page would look as follows:
Signup and login have been successful. However, if we tried to directly access http://localhost:3000/reviews without signing in, we'd be able to, since our home and reviews pages are still not under any type of authentication.
before_action :authenticate_user!
This one line will mean that access to any action in our controller needs to be authorized by devise. Here, authenticate_user! is a helper method that determines whether require is coming after authentication or not.
Note
before_action here is a helper method that is part of a filter. Filters in Rails are methods that are executed before or after a controller action. Filters are inherited from ApplicationController and will work with every controller of your application.
Now that we have covered both our Home and Reviews controllers, if you try to access any URL in our application, it will operate under an authentication system.
Solution
Part I – Create a blog application with the following features
$ rails new blog
The output should be as follows:
$ cd blog
$ rails generate scaffold post title:string body:text
The output should be as follows:
$ rails generate scaffold comment post_id:integer body:text
The output should be as follows:
$ rails db:migrate
The output should be as follows:
Rails.application.routes.draw do
resources :comments
resources :posts
root "posts#index"
end
Here, we have created a root route, which is the index page of the posts controller and will display a list of all blog posts.
$ rails server
The output should be as follows:
Part II – Deploying your application to Heroku
Before you start deploying your application to Heroku, you need to first create a free account with the Heroku CLI set up locally:
Fill in the simple form and create your free account:
Note
For our application, we won't require any paid add-ons. This application is available for access for anyone, free of cost.
With Homebrew:
brew tap heroku/brew && brew install heroku
With curl:
curl https://cli-assets.heroku.com/install.sh | sh
With npm (the least recommended):
npm install -g heroku
Check whether the installation is correct:
heroku --version
heroku/7.0.0 (darwin-x64) node-v8.0.0
Log into the Heroku CLI:
heroku login
heroku: Press any key to open up the browser to login or q to exit
' Warning: If browser does not open, visit
' https://cli-auth.heroku.com/auth/browser/***
heroku: Waiting for login...
Logging in... done
Logged in as [email protected]
gem 'pg'
To remove sqlite3, use the following command:
gem 'sqlite3'
Run the following command from the Terminal window:
$ bundle Install
Note
To set up a PostgreSQL database, please refer to the docs: https://packt.live/2MBhC4F.
default: &default
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# http://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
<<: *default
database: myapp_production
username: myapp
password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>
Since the application was running previously, we have to update it with the proper postgres configuration.
$ rails db:create
Terminal will show the following:
This has created our database. Next, let's run migrations:
$ rails db:migrate
We have successfully migrated our application from sqlite to postgres.
Thanks to the Active Record ORM( Object Relational Mapping), switching between databases is made very simple. As we saw in this exercise, migrating from sqlite3 to postgres is fast and easy.
ruby "2.6.0"
Git is required for deploying an application on Heroku. If you don't have Git on your machine, please set it up from https://git-scm.com/ (we won't be doing that since it's beyond the scope of this book).
Run the following command from the root of your application:
$ git add .
$ git commit -m "init"
$ heroku create
This automatically creates a Heroku application on the Heroku server with a deployment pipeline. It also creates a URL for it as you can see at https://protected-tor-47977.herokuapp.com/. After deployment, we will be able to access our application from this URL.
Note
The Heroku application URL for the live site will be different for you than the one mentioned in this activity.
$ git push heroku master
The output will show up as follows:
The deployment runs for a few hundred lines and takes roughly 30-40 seconds.
$ heroku run rails db:migrate
The output will show up as follows on Terminal:
Visit the following URL in your browser to check that your application is live on the World Wide Web; however, when you push the application, this URL will be different – this is a randomly generated URL by Heroku: https://protected-tor-47977.herokuapp.com/
The blog post will now show up as follows:
We have successfully hosted our application for the world to play with.
18.116.80.34