TwentyFortyEight 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dd1e0f1d57806e35b302242178e84246f788f6e2
4
+ data.tar.gz: 559e666fa0cfd84c1986a7fb0618324570706975
5
+ SHA512:
6
+ metadata.gz: 818a1636527209ef35db2ae0d3629209181ce4c26508ba43a2fe1737b3836d62c85f5c579366dde9953e8296cc7af643f39af6fe2856c13728c45f867deaacfd
7
+ data.tar.gz: '069c5d9fd01bda5d83db715ca50c6cf8ccecc78177c3d6e3e6554d4ece02596979a5fa5952b7ef0bcad86f012860199719c6116d2d740918727b30f8d08dc4d2'
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.14.1
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at sidneyliebrand@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in TwentyFortyEight.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Sidney Liebrand
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # TwentyFortyEight
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/TwentyFortyEight`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+ TODO: Write docs
7
+ TODO: Write tests
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'TwentyFortyEight'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install TwentyFortyEight
24
+
25
+ ## Usage
26
+
27
+ TODO: Write usage instructions here
28
+
29
+ ## Development
30
+
31
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
32
+
33
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
34
+
35
+ ## Contributing
36
+
37
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Sidney Liebrand/TwentyFortyEight. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
38
+
39
+
40
+ ## License
41
+
42
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'TwentyFortyEight/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'TwentyFortyEight'
8
+ spec.version = TwentyFortyEight::VERSION
9
+ spec.authors = ['Sidney Liebrand']
10
+ spec.email = ['sidneyliebrand@gmail.com']
11
+
12
+ spec.summary = %(A 2048 game for terminals)
13
+ spec.description = 'Play a game of 2048 in the terminal, colorized using ' \
14
+ 'Ruby curses. See --help for more options)'
15
+ spec.homepage = 'https://sidofc.github.io/projects/2048'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = "bin"
22
+ spec.executables = ['2048']
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.14'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.0'
28
+ spec.add_development_dependency 'pry'
29
+
30
+ spec.add_runtime_dependency 'curses'
31
+ spec.add_runtime_dependency 'json'
32
+ end
data/bin/2048 ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/TwentyFortyEight'
3
+
4
+ opts = TwentyFortyEight::Cli.parse!
5
+ TwentyFortyEight.send opts.mode, opts do
6
+ down || left || right || up
7
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "TwentyFortyEight"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,107 @@
1
+ require 'json'
2
+ require 'optparse'
3
+ require 'curses'
4
+ require_relative 'TwentyFortyEight/version'
5
+ require_relative 'TwentyFortyEight/options'
6
+ require_relative 'TwentyFortyEight/logger'
7
+ require_relative 'TwentyFortyEight/board'
8
+ require_relative 'TwentyFortyEight/game'
9
+ require_relative 'TwentyFortyEight/cli'
10
+ require_relative 'TwentyFortyEight/screen'
11
+ require_relative 'TwentyFortyEight/dsl'
12
+
13
+ module TwentyFortyEight
14
+ @@games = []
15
+ @@best = nil
16
+
17
+ def self.play(settings = {}, &block)
18
+ game = Game.new @@games.count, settings
19
+ dirs = game.directions - (settings.except || [])
20
+ dirs = dirs - (settings.only || [])
21
+ dsl = Dsl.new settings, &block if block_given?
22
+
23
+ @@best ||= game
24
+
25
+ Screen.init! settings if settings.verbose? && @@games.empty?
26
+
27
+ trap 'SIGINT' do
28
+ Screen.restore! if settings.verbose?
29
+ exit
30
+ end
31
+
32
+ restart = false
33
+ non_blocking = dirs
34
+
35
+ render_game game, settings
36
+
37
+ loop do
38
+ @@best = game if @@best != game && game.score > @@best.score
39
+
40
+ if game.end?
41
+ break if settings.mode? :endless
42
+
43
+ render_game game, settings, true
44
+
45
+ action = :default
46
+ until [:quit, :restart].include? action
47
+ action = Screen.handle_keypress
48
+ sleep 0.1
49
+ end
50
+
51
+ restart = true if action == :restart
52
+ break
53
+ else
54
+ if settings.interactive?
55
+ action = Screen.handle_keypress until Game::ACTIONS.include?(action)
56
+ else
57
+ action = dsl && dsl.apply(game.dup) || non_blocking.sample
58
+ non_blocking = game.changed? ? dirs : non_blocking - [action]
59
+
60
+ if non_blocking.empty?
61
+ non_blocking.concat settings.except if settings.except?
62
+ non_blocking.concat game.directions if settings.only?
63
+ end
64
+ end
65
+
66
+ render_game game.action(action), settings if settings.verbose? ||
67
+ settings.interactive?
68
+ sleep(settings.delay.to_f / 1000) if settings.delay?
69
+ end
70
+ end
71
+
72
+ @@games << game
73
+
74
+ if settings.log?
75
+ game.log.write! dir: 'logs',
76
+ name: "2048-#{Time.now.to_i}-#{game.id}-#{game.score}"
77
+ end
78
+
79
+ return play(settings, &block) if restart
80
+
81
+ game
82
+ ensure
83
+ Screen.restore! if settings.verbose? && settings.mode?(:play)
84
+ end
85
+
86
+ def self.endless(settings = {}, &block)
87
+ loop { TwentyFortyEight.play settings, &block }
88
+ ensure
89
+ Screen.restore! if settings.verbose?
90
+ end
91
+
92
+ def self.modes
93
+ (TwentyFortyEight.methods - [:modes, :render_game]) - Object.methods
94
+ end
95
+
96
+ def self.render_game(game, settings, final = false)
97
+ print_extra = { interactive: settings.interactive?,
98
+ info: [{ highscore: @@best&.score},
99
+ { score: game.score, dir: game.current_dir},
100
+ { id: @@games.count, move: game.move_count }]}
101
+
102
+ print_extra[:history] = (@@games + [game]) if settings.history?
103
+
104
+ return Screen.game_over game, print_extra if final
105
+ Screen.render game.board.to_a, print_extra
106
+ end
107
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ module TwentyFortyEight
3
+ # Board
4
+ class Board
5
+ attr_reader :board, :settings
6
+
7
+ def initialize(opts = {})
8
+ @settings = opts
9
+ @settings[:size] = settings.board.size if settings.board?
10
+ @board = settings.board || Board.generate(settings)
11
+ end
12
+
13
+ def set!(value, **opts)
14
+ @board[opts[:y]][opts[:x]] = value if opts[:x] && opts[:y]
15
+ end
16
+
17
+ def transpose!
18
+ replace! board.transpose
19
+ end
20
+
21
+ def replace!(board_arr)
22
+ @board = board_arr
23
+ end
24
+
25
+ def full?
26
+ empty_cells.empty?
27
+ end
28
+
29
+ def to_a
30
+ board
31
+ end
32
+
33
+ def empty_cells
34
+ board.each_with_index.map do |col, y|
35
+ col.each_with_index.map do |val, x|
36
+ { x: x, y: y } if settings.empty? val
37
+ end.compact
38
+ end.flatten
39
+ end
40
+
41
+ def dup
42
+ Board.new settings.merge(board: board.dup)
43
+ end
44
+
45
+ private
46
+
47
+ def self.generate(**opts)
48
+ Array.new(opts[:size]) do
49
+ Array.new(opts[:size]) { opts[:fill] }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+ module TwentyFortyEight
3
+ # Cli
4
+ module Cli
5
+ def self.parse!(**user_defaults)
6
+ mode = ARGV[0].to_s.downcase.to_sym
7
+ settings = defaults_for(mode).merge user_defaults
8
+ settings[:mode] = mode
9
+ settings[:mode] = :play unless TwentyFortyEight.modes.include? mode
10
+
11
+ OptionParser.new do |cli|
12
+ cli.banner = "2048 [mode] [options]"
13
+
14
+ cli.separator "options:"
15
+
16
+ cli.on('-i', '--interactive', 'Can you reach the 2048 tile?') do
17
+ settings[:interactive] = true
18
+ end
19
+
20
+ cli.on('-eX,Y,Z', '--exclude=X,Y,Z', Array,
21
+ 'Exclude directions') do |list|
22
+ settings[:except] = list.map(&:to_sym)
23
+ end
24
+
25
+ cli.on('-oX,Y,Z', '--only=X,Y,Z', Array,
26
+ 'Include directions') do |list|
27
+ settings[:only] = list.map(&:to_sym)
28
+ end
29
+
30
+ cli.on('-dMS', '--delay=MS', Float,
31
+ 'Delay in ms, applied after each move') do |ms|
32
+ settings[:delay] = ms
33
+ end
34
+
35
+ cli.on('-aSOLVER', '--a=SOLVER', String,
36
+ 'Specify a solver to play the game') do |solver|
37
+ settings[:solver] = solver.to_sym
38
+ end
39
+
40
+ cli.on('-sSIZE', '--size=SIZE', Integer,
41
+ 'Set grid size of the board') do |size|
42
+ settings[:size] = size
43
+ end
44
+
45
+ cli.on('-l', '--log', 'Log game moves in json format') do |v|
46
+ settings[:log] = v
47
+ end
48
+
49
+ cli.on('-h', '--history', 'Show game history') do |v|
50
+ settings[:history] = v
51
+ end
52
+
53
+ cli.on('-v', '--[no-]verbose', 'Toggles printing the game') do |v|
54
+ settings[:verbose] = v
55
+ end
56
+
57
+ cli.on('--help', 'Display this help') do
58
+ puts cli
59
+ exit 0
60
+ end
61
+ end.parse!
62
+
63
+ settings.delete :only if settings[:except]
64
+ settings.delete :except if settings[:only]
65
+
66
+ Options.new settings
67
+ end
68
+
69
+ def self.defaults_for(mode)
70
+ case mode.to_sym
71
+ when :endless
72
+ { verbose: true, delay: 100 }
73
+ when :play
74
+ { verbose: true, delay: 100 }
75
+ else
76
+ { verbose: true, interactive: true, delay: 10 }
77
+ end
78
+ end
79
+
80
+ class UnknownModeError < StandardError; end
81
+ end
82
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ module TwentyFortyEight
3
+ # Dsl
4
+ class Dsl
5
+ attr_reader :settings, :game
6
+
7
+ def initialize(settings = {}, &block)
8
+ @callable = block
9
+ @settings = settings
10
+ end
11
+
12
+ def apply(game)
13
+ @queue = []
14
+ @game = game.dup
15
+
16
+ instance_eval(&@callable)
17
+ end
18
+
19
+ def method_missing(sym, *args, &block)
20
+ return sym if game.action(sym, insert: false).changed?
21
+ end
22
+
23
+ def respond_to_missing?(sym, *args, &block)
24
+ true
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+ module TwentyFortyEight
3
+ # Game
4
+ class Game
5
+ attr_reader :id, :board, :settings, :score, :prev_score, :prev_available,
6
+ :move_count, :log, :current_dir
7
+
8
+ SETTINGS = { size: 4, fill: 0, empty: 0 }.freeze
9
+ MOVES = [:up, :down, :left, :right].freeze
10
+ ACTIONS = [*MOVES, :quit].freeze
11
+
12
+ def initialize(id = 1, opts = {}, **rest_opts)
13
+ @id = id
14
+ @score = 0
15
+ @prev_score = 0
16
+ @move_count = 0
17
+ @settings = Options.new SETTINGS.merge(opts).merge(rest_opts)
18
+ @board = Board.new(settings)
19
+ @prev_available = available
20
+ @current_dir = nil
21
+ @force_quit = false
22
+ @log = Logger.new if settings.log?
23
+ 2.times { insert! } unless settings.board?
24
+ end
25
+
26
+ def insert!
27
+ value = Random.rand(1..10) == 10 ? 4 : 2
28
+ pos = available.sample
29
+
30
+ board.set! value, pos if pos
31
+ end
32
+
33
+ def changed?
34
+ score > prev_score || (prev_available - available).any?
35
+ end
36
+
37
+ def won?
38
+ board.flatten.max >= 2048
39
+ end
40
+
41
+ def lost?
42
+ true if end? && !won?
43
+ end
44
+
45
+ def quit!
46
+ @force_quit = true
47
+ end
48
+
49
+ def end?
50
+ true if @force_quit || !mergeable? && board.full?
51
+ end
52
+
53
+ def mergeable?
54
+ directions.select { |dir| dup.move(dir, insert: false).changed? }.any?
55
+ end
56
+
57
+ def directions
58
+ MOVES
59
+ end
60
+
61
+ def available
62
+ board.empty_cells
63
+ end
64
+
65
+ def action(action, **opts)
66
+ action == :quit && quit! || move(action, opts)
67
+ self
68
+ end
69
+
70
+ def move(dir, **opts)
71
+ return self unless directions.include? dir
72
+
73
+ @prev_score = score
74
+ @prev_available = available
75
+ @current_dir = dir
76
+
77
+ send dir
78
+
79
+ if changed?
80
+ @move_count += 1
81
+
82
+ log << { move: move_count, score: score, direction: dir } if log
83
+ insert! unless opts[:insert] == false
84
+ end
85
+
86
+ self
87
+ end
88
+
89
+ def up
90
+ board.transpose! && left && board.transpose!
91
+ end
92
+
93
+ def down
94
+ board.transpose! && right && board.transpose!
95
+ end
96
+
97
+ def left
98
+ board.replace! board.to_a.map { |col| merge(col) }
99
+ end
100
+
101
+ def right
102
+ board.replace! board.to_a.map { |col| merge(col.reverse).reverse }
103
+ end
104
+
105
+ def dup
106
+ TwentyFortyEight::Game.new settings.merge(board: board.to_a)
107
+ end
108
+
109
+ private
110
+
111
+ def merge(unmerged)
112
+ input = unmerged.reject { |v| settings.empty? v }
113
+ output = []
114
+
115
+ while (current = input.shift)
116
+ compare = input.shift
117
+
118
+ if current == compare
119
+ merged = current << 1
120
+ @score += merged
121
+ output << merged
122
+ else
123
+ output << current
124
+ break unless compare
125
+ input.unshift compare if compare
126
+ end
127
+
128
+ end
129
+
130
+ output.concat(Array.new(unmerged.size - output.size) { settings.empty })
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ module TwentyFortyEight
3
+ # Logger
4
+ class Logger
5
+ attr_reader :entries
6
+
7
+ def initialize
8
+ @entries = []
9
+ end
10
+
11
+ def <<(info_hsh)
12
+ entries << { time: (Time.now.to_f * 1000).to_i, info: info_hsh }
13
+ end
14
+
15
+ def write!(options = {})
16
+ name = (options[:name] || "2048-#{Time.now.to_i}") + '.log.json'
17
+ path = File.expand_path(options[:path]) if options[:path]
18
+ path = File.join(Dir.pwd, name) unless options[:dir]
19
+ path = (File.join Dir.pwd, options[:dir], name) if options[:dir]
20
+
21
+ File.open(path, 'w') { |f| f.write @entries.to_json }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,47 @@
1
+ module TwentyFortyEight
2
+ # Options
3
+ class Options
4
+ def initialize(options = {})
5
+ @options = options
6
+ end
7
+
8
+ def [](key)
9
+ @options[key]
10
+ end
11
+
12
+ def method_missing(sym, *args, &block)
13
+ return @options[to_option(sym)] == args.first if args.any?
14
+ @options[to_option(sym)]
15
+ end
16
+
17
+ def respond_to_missing?(sym, *args, &block)
18
+ option? sym
19
+ end
20
+
21
+ def merge(other)
22
+ @options.merge! other
23
+ end
24
+
25
+ def to_hash
26
+ @options
27
+ end
28
+
29
+ def to_a
30
+ @options.to_a
31
+ end
32
+
33
+ def to_s
34
+ @options.to_s
35
+ end
36
+
37
+ private
38
+
39
+ def to_option(sym)
40
+ sym.to_s.tr('?', '').to_sym
41
+ end
42
+
43
+ def option?(sym)
44
+ @options.keys.include? to_option(sym)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+ module TwentyFortyEight
3
+ # Screen
4
+ module Screen
5
+ COLOR_MAP = {
6
+ # white
7
+ 255 => [7, 7],
8
+ # green
9
+ 70 => [7, 70], 34 => [7, 34], 28 => [7, 28], 22 => [7, 22],
10
+ # blue
11
+ 39 => [7, 39], 33 => [7, 33], 27 => [7, 27], 21 => [7, 21],
12
+ # magenta
13
+ 134 => [7, 134], 128 => [7, 128], 91 => [7, 91], 45 => [7, 54],
14
+ # red
15
+ 196 => [7, 196], 160 => [7, 160], 124 => [7, 124],
16
+ # orange
17
+ 208 => [7, 208], 202 => [7, 202], 166 => [7, 166],
18
+ # yellow
19
+ 228 => [0, 228], 227 => [0, 227], 226 => [0, 226], 220 => [0, 220]
20
+ }.freeze
21
+
22
+ # create [tile val] => [color] map to color tiles
23
+ COLOR_MAP_V = COLOR_MAP.keys.each_with_index.map do |color, shift|
24
+ [(shift.zero? && shift || (1 << shift)), color]
25
+ end.to_h.freeze
26
+
27
+ CELL_PADDING = 10
28
+ CELL_HEIGHT = CELL_PADDING / 3
29
+ HIST_WIDTH = 15
30
+
31
+ def self.init!(options = {})
32
+ Curses.init_screen
33
+ Curses.cbreak
34
+ Curses.noecho
35
+ Curses.nonl
36
+ Curses.curs_set 0
37
+ Curses.timeout = 0
38
+ Curses.stdscr.keypad true
39
+
40
+ init_colors! if Curses.has_colors?
41
+
42
+ trap('SIGINT') { restore! && exit }
43
+ end
44
+
45
+ def self.restore!
46
+ Curses.curs_set 1
47
+ Curses.clear
48
+ Curses.close_screen
49
+ end
50
+
51
+ def self.game_over(game, options = {})
52
+ hist = options[:history] || []
53
+ info = options[:info] || []
54
+
55
+ bw, hw, _hh, sy, sx = render_offsets hist, info, game.board.to_a
56
+
57
+ render_game_over game, (sx - hw / 2), (sy + info.size)
58
+ end
59
+
60
+ def self.render(board, options = {})
61
+ hist = options[:history] || []
62
+ info = options[:info] || []
63
+
64
+ bw, hw, hh, sy, sx = render_offsets hist, info, board
65
+
66
+ render_history hist, (sx + bw + 2 - hw / 2), sy, hw, hh if hist.any?
67
+ render_info info, (sx - hw / 2), sy, bw if info.any?
68
+
69
+ render_board board, (sx - hw / 2), (sy + info.size)
70
+ handle_keypress options[:interactive]
71
+
72
+ Curses.refresh
73
+ end
74
+
75
+ def self.render_offsets(history, info, b)
76
+ hist_size = history.any? ? HIST_WIDTH : 0
77
+ board_width = CELL_PADDING * b.size
78
+
79
+ [board_width, hist_size, (b.size * CELL_HEIGHT + info.size),
80
+ (Curses.lines / 2) - ((board_width / 2) / CELL_HEIGHT),
81
+ (Curses.cols / 2) - (board_width / 2)]
82
+ end
83
+
84
+ def self.handle_keypress(allow_moves = true)
85
+ case Curses.getch
86
+ when ' ' then sleep 0.2 until Curses.getch == ' '
87
+ when Curses::KEY_DOWN, 's' then :down if allow_moves
88
+ when Curses::KEY_UP, 'w' then :up if allow_moves
89
+ when Curses::KEY_LEFT, 'a' then :left if allow_moves
90
+ when Curses::KEY_RIGHT, 'd' then :right if allow_moves
91
+ when Curses::KEY_CLOSE, 'q' then :quit
92
+ when 'r' then :restart
93
+ end
94
+ end
95
+
96
+ def self.render_history(history, start_x, start_y, width = 15, size = 10)
97
+ history.last(size).reverse.each_with_index do |game, y|
98
+ Curses.setpos (start_y + y), start_x
99
+ Curses.attron (Curses.color_pair(0) | Curses::A_BOLD) do
100
+ Curses.addstr justified_str("##{game.id}", game.score, width)
101
+ end
102
+ end
103
+ end
104
+
105
+ def self.render_info(info_rows, start_x, start_y, width)
106
+ info_rows.each_with_index do |h, y|
107
+ part_width = width / h.count
108
+ h.each_with_index do |(label, value), x|
109
+ xx = x > 0 ? 1 : 0
110
+ Curses.setpos (start_y + y), start_x + xx + (x * part_width)
111
+ Curses.attron (Curses.color_pair(0) | Curses::A_BOLD) do
112
+ Curses.addstr justified_str(label, value, (part_width - xx))
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ def self.render_board(board, start_x, start_y)
119
+ board.each_with_index do |col, y|
120
+ current_y = start_y + y * CELL_HEIGHT
121
+ col.each_with_index do |val, x|
122
+ current_x = start_x + x * CELL_PADDING
123
+ Curses.attron (Curses.color_pair(color_from_value(val)) | Curses::A_BOLD) do
124
+ cell(val).each_with_index do |line, offset|
125
+ Curses.setpos (current_y + offset), current_x
126
+ Curses.addstr line
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ def self.render_game_over(game, start_x, start_y)
134
+ size = game.board.to_a.size
135
+ width = size * CELL_PADDING
136
+ spacer = ''.center width
137
+ lines = ['Game over!', "score: #{game.score}", '', "[Q]uit", "[R]estart"]
138
+ lines = lines.map { |text| text.center width }
139
+ rows = (size * CELL_HEIGHT - lines.count).to_f
140
+
141
+ (rows / 2).floor.times { lines.unshift spacer }
142
+ (rows / 2).ceil.times { lines.push spacer }
143
+
144
+ Curses.attron (Curses.color_pair(250) | Curses::A_BOLD) do
145
+ lines.each_with_index do |line, offset|
146
+ Curses.setpos (start_y + offset), start_x
147
+ Curses.addstr line
148
+ end
149
+ end
150
+ end
151
+
152
+ def self.color_from_value(v)
153
+ COLOR_MAP_V[v] || COLOR_MAP.keys.last
154
+ end
155
+
156
+ def self.cell(val, width = CELL_PADDING, fill_count = CELL_HEIGHT, r = 0)
157
+ spacer = ''.center width
158
+ lines = [(val > r ? val : '').to_s.center(width)]
159
+ fill_count = fill_count > 1 ? fill_count - 1 : fill_count
160
+
161
+ (fill_count / 2).ceil.times { lines.unshift spacer }
162
+ (fill_count / 2).floor.times { lines.push spacer }
163
+
164
+ lines
165
+ end
166
+
167
+ def self.justified_str(label, value, length, seperator = ': ')
168
+ label_width = label.to_s.size + seperator.size
169
+ "#{label}#{seperator}#{value.to_s.rjust length - label_width}"
170
+ end
171
+
172
+ def self.init_colors!
173
+ Curses.start_color
174
+ Curses.assume_default_colors -1, -1
175
+
176
+ Curses.init_pair 250, 0, 7
177
+
178
+ COLOR_MAP.each_with_index do |arr, i|
179
+ Curses.init_pair arr[0], *arr[1]
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,3 @@
1
+ module TwentyFortyEight
2
+ VERSION = '0.1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: TwentyFortyEight
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sidney Liebrand
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: curses
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: json
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Play a game of 2048 in the terminal, colorized using Ruby curses. See
98
+ --help for more options)
99
+ email:
100
+ - sidneyliebrand@gmail.com
101
+ executables:
102
+ - '2048'
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".rspec"
108
+ - ".travis.yml"
109
+ - CODE_OF_CONDUCT.md
110
+ - Gemfile
111
+ - LICENSE.txt
112
+ - README.md
113
+ - Rakefile
114
+ - TwentyFortyEight.gemspec
115
+ - bin/2048
116
+ - bin/console
117
+ - bin/setup
118
+ - lib/TwentyFortyEight.rb
119
+ - lib/TwentyFortyEight/board.rb
120
+ - lib/TwentyFortyEight/cli.rb
121
+ - lib/TwentyFortyEight/dsl.rb
122
+ - lib/TwentyFortyEight/game.rb
123
+ - lib/TwentyFortyEight/logger.rb
124
+ - lib/TwentyFortyEight/options.rb
125
+ - lib/TwentyFortyEight/screen.rb
126
+ - lib/TwentyFortyEight/version.rb
127
+ homepage: https://sidofc.github.io/projects/2048
128
+ licenses:
129
+ - MIT
130
+ metadata: {}
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubyforge_project:
147
+ rubygems_version: 2.6.8
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: A 2048 game for terminals
151
+ test_files: []