Skip to content

nanenj/ruby-cookie-clicker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

So, a post by deebeer was sharing that he had re-written cookie clicker in csharp as a way of learning. I'm not learning so much as I just thought it would be a fun exercise to try and recreate the game in my current language of choice as well as possibly alluding to some things that could improve his C# version. I figured as I was doing it, I could also type up a guide for those who might want to learn some ruby or something. :)

For those that are unfamiliar with cookie clicker, it's a game with a giant cookie image on one side of the screen. You can click the cookie to get a cookie. At the start of the game, every click of the cookie nets you just a single cookie. As you progress in the game you can buy research that gives you more cookies per click. After amassing a small amount of cookies, you can buy things that will generate cookies for you. The first object is a cookie clicker that 'clicks' the cookie once every 10 seconds. There are a number of these style of cookie generators and each produces cookies at a different rate. They also each have different research associated with them for increasing their effectiveness. At random, golden cookies that give you a temporary boost may also appear on screen to be clicked.

The majority of the game has to deal with the cookie generators, you typically only end up clicking cookies enough to get started and very soon after the generators are making cookies faster than you can click and clicking only provides a marginal boost to your overall cookies per second. Since I'm coding this in ruby, I'm going to skip the clickable bits at the moment. We can model the majority of the game without needing to click a single thing.

So, I think a good place to start would be to define an object that generates cookies. I'll create a cookie_clicker.rb file and define a class and give it a few attributes.

class CookieGenerator
  attr_accessor :name, :description, :rate, :cost
end

This is a good first stab at what the base CookieGenerator might look like. There's a number of such objects in cookie clicker. The first as I said above the game is the namesake of the game, the cookie clicker. It can be defined as follows. For the sake of making things easier on myself, I kept this inside the cookie_clicker.rb file for now.

class CookieClicker < CookieGenerator
  def initialize
    @name = "Cookie Clicker"
    @description = "A clicker that generates 1 cookie every 10 seconds."
    @rate = 0.1
    @cost = 15
  end
end

At this point, I can start up irb and require './cookie_clicker.rb' and play around with our new class.

[cookie_clicker] irb

1.9.3-p385 :001 > require './cookie_clicker.rb'
 => true
1.9.3-p385 :002 > CookieGenerator
 => CookieGenerator
1.9.3-p385 :003 > CookieClicker
 => CookieClicker
1.9.3-p385 :004 > CookieClicker.new
 => #<CookieClicker:0x007f925d896fb0 @name="Cookie Clicker", @description="A clicker that generates 1 cookie every 10 seconds.", @rate=0.1, @cost=15>
1.9.3-p385 :005 > c = _
 => #<CookieClicker:0x007f925d896fb0 @name="Cookie Clicker", @description="A clicker that generates 1 cookie every 10 seconds.", @rate=0.1, @cost=15>
1.9.3-p385 :006 > c.name
 => "Cookie Clicker"
1.9.3-p385 :007 > c.rate
 => 0.1
1.9.3-p385 :008 > c.cost
 => 15

Not all that exciting just yet, but, we've actually done something pretty neat with having a base class and a class inheriting from it. All of our types of CookieGenerators will have similar attributes and we can probably share a number of functions among them too. Right now, our cookie generator doesn't actually generate cookies though. Let's fix that up.

In our CookieGenerator class, we're going to add a new attribute :created_at and we're going to make a function to return how many cookies the generator has made. The rounding may make things ever so slightly inaccurate, but floating point numbers suck.

class CookieGenerator
  attr_accessor :name, :description, :rate, :cost, :created_at

  def cookies_generated
    return ((Time.now - @created_at).round * @rate).round(2)
  end
end

In our CookieClicker class, we're going to initialize the attribute :created_at with Time.now

class CookieClicker < CookieGenerator
  def initialize
    @name = "Cookie Clicker"
    @description = "A clicker that generates 1 cookie every 10 seconds."
    @rate = 0.1
    @cost = 15
    @created_at = Time.now
  end
end

With this done, we can play with irb quickly again and get a little bit more exciting of a result.

1.9.3-p385 :001 > require './cookie_clicker.rb'
 => true
1.9.3-p385 :002 > c = CookieClicker.new
 => #<CookieClicker:0x007ff56a095f58 @name="Cookie Clicker", @description="A clicker that generates 1 cookie every 10 seconds.", @rate=0.1, @cost=15, @created_at=2013-10-09 18:21:13 -0500>
1.9.3-p385 :003 > Time.now
 => 2013-10-09 18:21:16 -0500
1.9.3-p385 :004 > Time.now
 => 2013-10-09 18:21:31 -0500
1.9.3-p385 :005 > c.cookies_generated
 => 2.4
1.9.3-p385 :006 > Time.now
 => 2013-10-09 18:21:40 -0500
1.9.3-p385 :007 > c.cookies_generated
 => 2.9

Sweet, we can make a CookieClicker and it's generating cookies! In the game, you typically end up with a ton of these objects... let's hop back in irb and see what we can do there.

1.9.3-p385 :001 > require './cookie_clicker.rb'
 => true
1.9.3-p385 :002 > CookieGenerators = []
 => []
1.9.3-p385 :003 > 6.times do
1.9.3-p385 :004 >     CookieGenerators << CookieClicker.new
1.9.3-p385 :005?>   end
 => 6
1.9.3-p385 :006 > CookieGenerators
 => [#<CookieClicker:0x007fff4c8b24c0 @name="Cookie Clicker", @description="A clicker that generates 1 cookie every 10 seconds.", @rate=0.1, @cost=15, @created_at=2013-10-09 21:25:33 -0500>, ... SNIP! ...]
1.9.3-p385 :007 > CookieGenerators.inject(0) { |sum, i|
1.9.3-p385 :008 >     sum + i.cookies_generated
1.9.3-p385 :009?>   }
 => 129.6

We can create a collection of CookieGenertor objects and then sum all their cookies generated. Let's see if this works as good as I hope by making another type of CookieGenerator.

class CookieBaker < CookieGenerator
  def initialize
    @name = "Cookie Baker"
    @description = "A dedicated baker producing a dozen cookies a second."
    @rate = 12
    @cost = 500
    @created_at = Time.now
  end
end
1.9.3-p385 :001 > require './cookie_clicker.rb'
 => true
1.9.3-p385 :002 > cookiegenerators = []
 => []
1.9.3-p385 :003 > 6.times do
1.9.3-p385 :004 >     cookiegenerators << CookieClicker.new
1.9.3-p385 :005?>   cookiegenerators << CookieBaker.new
1.9.3-p385 :006?>   end
 => 6
1.9.3-p385 :007 > cookiegenerators
 => [#<CookieClicker:0x007fe25b0c1af8 @name="Cookie Clicker", @description="A clicker that generates 1 cookie every 10 seconds.", @rate=0.1, @cost=15, @created_at=2013-10-09 21:50:13 -0500>, ... SNIP! ... #<CookieBaker:0x007fe25b0c0bf8 @name="Cookie Baker", @description="A dedicated baker producing a dozen cookies a second.", @rate=12, @cost=500, @created_at=2013-10-09 21:50:13 -0500>]
1.9.3-p385 :008 > cookiegenerators.inject(0) { |sum, i|
1.9.3-p385 :009 >     sum + i.cookies_generated
1.9.3-p385 :010?>   }
 => 2976.6
1.9.3-p385 :011 > quit

That works! So, we can get our cookies per second and the number of cookies generated by all our objects simply by adding them to an array and using a similar 'inject' for each. Let's define some extra methods and attributes in our CookieGenerator class to make some of this easier. We'll add a 'generators' attribute to the accessor and make it a class variable, all our generators will have access to this and it'll be the same across all of our generators.

class CookieGenerator
  attr_accessor :name, :description, :rate, :cost, :created_at, :generators
  @@generators = []

  def all
     @@generators
  end

  def total_cps
    @@generators.inject(0) { |sum, i|
      sum + i.rate
    }
  end

  def total_cookies_earned
    @@generators.inject(0) { |sum, i|
      sum + i.cookies_generated
    }
  end

  def cookies_generated
    return ((Time.now - @created_at).round * @rate).round(2)
  end

end

After that, it's back to irb again. It's really taking shape now.

1.9.3-p385 :001 > require './cookie_clicker.rb'
 => true
1.9.3-p385 :002 > CookieClicker.new
 => #<CookieClicker:0x007f86d8915bc0 @name="Cookie Clicker", @description="A clicker that generates 1 cookie every 10 seconds.", @rate=0.1, @cost=15, @created_at=2013-10-09 22:39:46 -0500>
1.9.3-p385 :003 > CookieBaker.new
 => #<CookieBaker:0x007f86d891a418 @name="Cookie Baker", @description="A dedicated baker producing a dozen cookies a second.", @rate=12, @cost=500, @created_at=2013-10-09 22:39:49 -0500>
1.9.3-p385 :004 > c = _
 => #<CookieBaker:0x007f86d891a418 @name="Cookie Baker", @description="A dedicated baker producing a dozen cookies a second.", @rate=12, @cost=500, @created_at=2013-10-09 22:39:49 -0500>
1.9.3-p385 :005 > c.all
 => [#<CookieClicker:0x007f86d8915bc0 @name="Cookie Clicker", @description="A clicker that generates 1 cookie every 10 seconds.", @rate=0.1, @cost=15, @created_at=2013-10-09 22:39:46 -0500>, #<CookieBaker:0x007f86d891a418 @name="Cookie Baker", @description="A dedicated baker producing a dozen cookies a second.", @rate=12, @cost=500, @created_at=2013-10-09 22:39:49 -0500>]
1.9.3-p385 :006 > c.total_cps
 => 12.1
1.9.3-p385 :007 > c.total_cookies_earned
 => 157.6
1.9.3-p385 :008 > c.total_cookies_earned
 => 181.8
1.9.3-p385 :009 > quit

So, a mild jump ahead while I quickly implement the rest of the cookie generator types. I'll post all the cookie generator types below, including the two we've already done.

class CookieClicker < CookieGenerator
  def initialize
    @name = "Cookie Clicker"
    @description = "A clicker that generates 1 cookie every 10 seconds."
    @rate = 0.1
    @cost = 15
    @created_at = Time.now
    @@generators << self
  end
end

class CookieBaker < CookieGenerator
  def initialize
    @name = "Cookie Baker"
    @description = "A dedicated baker producing a cookies every 2 seconds."
    @rate = 0.5
    @cost = 100
    @created_at = Time.now
    @@generators << self
  end
end

class CookieFarm < CookieGenerator
  def initialize
    @name = "Cookie Farm"
    @description = "A cookie farm producing 4 cookies a second."
    @rate = 4
    @cost = 500
    @created_at = Time.now
    @@generators << self
  end
end

class CookieFactory < CookieGenerator
  def initialize
    @name = "Cookie Factory"
    @description = "A cookie factory producing 10 cookies a second."
    @rate = 10
    @cost = 3000
    @created_at = Time.now
    @@generators << self
  end
end

class CookieMine < CookieGenerator
  def initialize
    @name = "Cookie Mine"
    @description = "A cookie mine producing 40 cookies a second."
    @rate = 40
    @cost = 10000
    @created_at = Time.now
    @@generators << self
  end
end

class CookieShipment < CookieGenerator
  def initialize
    @name = "Cookie Shipment"
    @description = "A cookie shipment from space producing 100 cookies a second."
    @rate = 100
    @cost = 40000
    @created_at = Time.now
    @@generators << self
  end
end

class CookieLabratory < CookieGenerator
  def initialize
    @name = "Cookie Labratory"
    @description = "A cookie labratory producing 400 cookies a second."
    @rate = 400
    @cost = 200000
    @created_at = Time.now
    @@generators << self
  end
end

class CookiePortal < CookieGenerator
  def initialize
    @name = "Cookie Portal"
    @description = "A cookie portal to the underworld producing 6,666 cookies a second."
    @rate = 6666
    @cost = 1666666
    @created_at = Time.now
    @@generators << self
  end
end

class CookieTimeMachine < CookieGenerator
  def initialize
    @name = "Cookie Factory"
    @description = "A cookie time machine producing 98,765 cookies a second."
    @rate = 98765
    @cost = 123456789
    @created_at = Time.now
    @@generators << self
  end
end

class CookieCondenser < CookieGenerator
  def initialize
    @name = "Cookie Condensor"
    @description = "A cookie condensor producing 999,999 cookies a second."
    @rate = 999999
    @cost = 3999999999
    @created_at = Time.now
    @@generators << self
  end
end

For those paying attention, they might realise that this is an awful lot of repeated code. What if there was a way of generating all of these with much less code? Well, there is a few ways we can reduce the amount of code for all these. At current in fact, there's no 'special' code unique to any of the objects. They could all be just a generic CookieGenerator with the appropriate attributes set on that particular object.

Also, in ruby, you can easily use code to generate code. So we could do something like....

%w[CookieClicker CookieBaker CookieFarm CookieFactory CookieMine CookieShipment CookieLabratory CookiePortal CookieTimeMachine CookieCondenser].each { |klass|

Object::const_set(klass, Class.new(CookieGenerator) {
  def initialize
    @@generators ||= []
    @name = self.class.to_s
    @description = "A #{@name} that produces cookies."
    @created_at = Time.now
    @@generators << self
  end
  })
}

This is pretty dang neat in my opinion. I don't want to dwell on this too much, as I'd rather continue with the project than theorycode forever, but, there's countless ways to accomplish the same thing. As I said before the snippet, we could even get away with simply leaving all the CookieGenerator objects as a CookieGenerator and part of me even argues that at current that's the better course and I'm breaking a programming idiom of 'you aren't gonna need it'. I just think that having them as different class types may allow me to do some things i want to do later more easily. I could be wrong and maybe someone that comes along and sees this can clue me in. :)

So, next, probably spending cookies. Someone with more foresight than I may have noticed something, I can't get the 'rate' or cost of any of our cookie generators without creating an instance of the object. A quick bout of changes to make each of our class definitions look like the below should fix that up.

class CookieClicker < CookieGenerator
  @name = "Cookie Clicker"
  @description = "A clicker that generates 1 cookie every 10 seconds."
  @rate = 0.1
  @cost = 15

  def initialize
    @name = "Cookie Clicker"
    @description = "A clicker that generates 1 cookie every 10 seconds."
    @rate = 0.1
    @cost = 15
    @created_at = Time.now
    @@generators << self
  end
end

I do wish I knew a better way of doing that, I suppose i could have done @name = self.name etc and it would have saved a little bit. I can't seem to figure out at the moment how to have an attribute both on self and on the instanced object. Anyone know?

Also, we'll add some code to the CookieGenerator class so that we can access those.

  def self.all
     @@generators
  end

  def self.rate
    @rate
  end

  def self.cost
    @cost
  end

  def self.name
    @name
  end

  def self.description
    @description
  end

So, next we might want to create a player object, to keep track of how many cookies are clicked and spent by the player. This one gets to be a small chunk of code pretty quick.

class Player
  attr_accessor :cookies_spent, :cookies_clicked

  def initialize
    @cookies_clicked = 0
    @cookies_spent = 0
  end

  def buyGenerator(generatorType, amount)
    cost_to_buy = ((Object::const_get(generatorType).cost) * amount)
    generator_to_buy = Object::const_get(generatorType).name

    if cookies > cost_to_buy
      puts "Buying #{amount} #{generator_to_buy}"
      @cookies_spent += cost_to_buy
      amount.times do
        Object::const_get(generatorType).new
      end

    else
      puts "You need #{(cost_to_buy - cookies).round} more cookies to buy #{amount} #{generator_to_buy}"
    end
  end

  def cookies
    return (((CookieGenerator.total_cookies_earned != nil ? CookieGenerator.total_cookies_earned : 0) + @cookies_clicked) - @cookies_spent).round(2)
  end

  def click
    @cookies_clicked += 1
  end
end

This (rather large) block of code, at least compared to some of the rest of the stuff we've done in setting this up implements a player object to buy cookie generators and does our simple 'click' to get a single cookie. It'll also increment a variable whenever cookies are spent, and it'll keep track of all cookies by reporting (CookieGenerator.total_cookies_earned + cookies_clicked) - cookies_spent . Yeah, it's not a great model, but, it works for the purposes of this particular game.

At this point, we have a semi-playable game/version of cookie clicker in irb, to demonstrate.

1.9.3-p385 :001 > require "./cookie_clicker.rb"
 => true
1.9.3-p385 :002 > p = Player.new
 => #<Player:0x007fa59986a0b0 @cookies_clicked=0, @cookies_spent=0>
1.9.3-p385 :003 > p.cookies
 => 0.0
1.9.3-p385 :004 > p.click
 => 1
1.9.3-p385 :005 > p.cookies
 => 1.0
1.9.3-p385 :006 > 15.times do
1.9.3-p385 :007 >     p.click
1.9.3-p385 :008?>   end
 => 15
1.9.3-p385 :009 > p.cookies
 => 16.0
1.9.3-p385 :010 > p.buyGenerator "CookieClicker", 1
Buying 1 Cookie Clicker
 => 1
1.9.3-p385 :011 > p.cookies
 => 1.3
1.9.3-p385 :012 > p.cookies
 => 1.6
1.9.3-p385 :013 >
1.9.3-p385 :014 >   CookieGenerator.all
 => [#<CookieClicker:0x007fa599941ee8 @name="Cookie Clicker", @description="A clicker that generates 1 cookie every 10 seconds.", @rate=0.1, @cost=15, @created_at=2013-10-10 12:21:44 -0500>]
1.9.3-p385 :015 > p.cookies
 => 11.8
1.9.3-p385 :016 > p.click
 => 17
1.9.3-p385 :017 > 500.times do
1.9.3-p385 :018 >     p.click
1.9.3-p385 :019?>   end
 => 500
1.9.3-p385 :020 > p.buyGenerator "CookieBaker", 5
Buying 5 Cookie Baker
 => 5
1.9.3-p385 :021 > p.cookies
 => 23.5
1.9.3-p385 :022 > CookieGenerator.all
 => [#<CookieClicker:0x007fa599941ee8 @name="Cookie Clicker", @description="A clicker that generates 1 cookie every 10 seconds.", @rate=0.1, @cost=15, @created_at=2013-10-10 12:21:44 -0500>, #<CookieBaker:0x007fa5999763a0 @name="Cookie Baker", @description="A dedicated baker producing a cookies every 2 seconds.", @rate=0.5, @cost=100, @created_at=2013-10-10 12:24:01 -0500>, #<CookieBaker:0x007fa599976300 @name="Cookie Baker", @description="A dedicated baker producing a cookies every 2 seconds.", @rate=0.5, @cost=100, @created_at=2013-10-10 12:24:01 -0500>, #<CookieBaker:0x007fa599976260 @name="Cookie Baker", @description="A dedicated baker producing a cookies every 2 seconds.", @rate=0.5, @cost=100, @created_at=2013-10-10 12:24:01 -0500>, #<CookieBaker:0x007fa5999761c0 @name="Cookie Baker", @description="A dedicated baker producing a cookies every 2 seconds.", @rate=0.5, @cost=100, @created_at=2013-10-10 12:24:01 -0500>, #<CookieBaker:0x007fa599976120 @name="Cookie Baker", @description="A dedicated baker producing a cookies every 2 seconds.", @rate=0.5, @cost=100, @created_at=2013-10-10 12:24:01 -0500>]
1.9.3-p385 :023 > p.cookies
 => 127.5
1.9.3-p385 :024 >

About

An exercise to create a clone of cookie clicker in ruby.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages