pentix 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Pentix In Ruby
2
+
3
+ Pentix is a morally superior Siberian version of the good old tetris game.
4
+ It uses `5x5` matrix and a collection of 29 advanced figures.
5
+
6
+ And yes, they won't allow you to operate a battle bear until you reach at
7
+ least level 15 in this game :)
8
+
9
+
10
+ ## Usage
11
+
12
+ You'll need `Ruby` and `gem install gosu`, after that just run
13
+ the `pentix.rb` file in the console
14
+
15
+
16
+ ## The Purpose
17
+
18
+ This project is made solely for educational and self-training purposes.
19
+ Also, as an example for the [gosu](http://code.google.com/p/gosu) gem.
20
+
21
+
22
+ ## License & Copyright
23
+
24
+ All the source code and media files in this project are released
25
+ under the terms of the
26
+ [Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License](http://creativecommons.org/licenses/by-nc-sa/3.0/)
27
+
28
+ Copyright (C) 2011 Nikolay Nemshilov
29
+
data/bin/pentix ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'pentix.rb'))
3
+ Game.new.show
data/lib/block.rb ADDED
@@ -0,0 +1,20 @@
1
+ #
2
+ # A simple block to be used in the figures and the glass
3
+ #
4
+ # Copyright (C) 2011 Nikolay Nemshilov
5
+ #
6
+ class Block
7
+ attr_accessor :color, :size
8
+
9
+ SIZE = 20 # block size in pixels
10
+ FILE = File.join(File.dirname(__FILE__), '..', 'media', 'block.png')
11
+
12
+ def initialize(window, color)
13
+ @@img ||= Image.new(window, FILE, true)
14
+ @color = color
15
+ end
16
+
17
+ def draw(x, y)
18
+ @@img.draw(x * SIZE, y * SIZE, 0, 1.0, 1.0, @color)
19
+ end
20
+ end
data/lib/controls.rb ADDED
@@ -0,0 +1,24 @@
1
+ #
2
+ # And here we handle the controls
3
+ #
4
+ # Copyright (C) 2011 Nikolay Nemshilov
5
+ #
6
+ class Controls
7
+
8
+ BUTTONS = {
9
+ :quit => Button::KbQ,
10
+ :reset => Button::KbEscape,
11
+ :drop => Button::KbSpace,
12
+ :left => Button::KbLeft,
13
+ :right => Button::KbRight,
14
+ :turn_left => Button::KbUp,
15
+ :turn_right => Button::KbDown
16
+ }.freeze
17
+
18
+ def command_for(button)
19
+ BUTTONS.each do |key, value|
20
+ return key if button === value
21
+ end
22
+ end
23
+
24
+ end
data/lib/figure.rb ADDED
@@ -0,0 +1,155 @@
1
+ #
2
+ # The figure unit. It handles the figure basics, like drawing itself
3
+ # making all sorts of manipulations like movements and rotations,
4
+ # plus it keeps the track of it's position and distance below
5
+ #
6
+ # Copyright (C) 2011 Nikolay Nemshilov
7
+ #
8
+ class Figure
9
+ attr_accessor :name, :color, :matrix, :pos_x, :pos_y, :distance
10
+
11
+ #
12
+ # Basic constructor
13
+ #
14
+ # NOTE! creates a random figure if there is no explicit config
15
+ #
16
+ def initialize(window, name=nil)
17
+ name ||= FIGURES.keys.shuffle[0]
18
+ config = FIGURES[name].split('|')
19
+
20
+ @pos_x = 0
21
+ @pos_y = 0
22
+
23
+ @name = name
24
+ @window = window
25
+ @color = COLORS[config.pop.to_sym]
26
+ @block = Block.new(window, @color)
27
+
28
+ @matrix = config.map{ |row| row.split('').map{|c| c == 'x'}}
29
+
30
+ # getting rid of empty colls and rows
31
+ @matrix.reject!{ |row| row.none? }
32
+
33
+ while @matrix.map{ |row| row.last }.none?
34
+ @matrix.each{ |row| row.pop }
35
+ end
36
+ end
37
+
38
+ #
39
+ # Draws the blocks of the figure
40
+ #
41
+ def draw
42
+ @matrix.each_with_index do |row, y|
43
+ row.each_with_index do |visible, x|
44
+ @block.draw(@pos_x + x, @pos_y + y) if visible
45
+ end
46
+ end
47
+ end
48
+
49
+ def size_x
50
+ @matrix[0].size
51
+ end
52
+
53
+ def size_y
54
+ @matrix.size
55
+ end
56
+
57
+ def drop
58
+ @window.glass.glue_in(self)
59
+ @window.show_next_figure
60
+ end
61
+
62
+ def move_to(x, y)
63
+ try_set(@matrix, x, y)
64
+ end
65
+
66
+ def move_left
67
+ move_to(@pos_x - 1, @pos_y)
68
+ end
69
+
70
+ def move_right
71
+ move_to(@pos_x + 1, @pos_y)
72
+ end
73
+
74
+ def move_down
75
+ move_to(@pos_x, @pos_y + 1)
76
+ end
77
+
78
+ def turn_left
79
+ new_matrix = (0..size_x-1).map do |i|
80
+ @matrix.map{ |row| row[size_x - 1 - i] }
81
+ end
82
+
83
+ try_set(new_matrix, @pos_x, @pos_y)
84
+ end
85
+
86
+ def turn_right
87
+ new_matrix = (0..size_x-1).map do |i|
88
+ @matrix.map{ |row| row[i] }.reverse
89
+ end
90
+
91
+ try_set(new_matrix, @pos_x, @pos_y)
92
+ end
93
+
94
+ protected
95
+
96
+ def try_set(matrix, pos_x, pos_y)
97
+ # adjusting x-position for tall figures
98
+ if matrix != @matrix
99
+ offset = (size_x - matrix[0].size).abs/2
100
+ pos_x += offset * (size_x > matrix[0].size ? 1 : -1)
101
+ end
102
+
103
+ if @window.glass.has_space_for?(matrix, pos_x, pos_y)
104
+ @pos_x = pos_x
105
+ @pos_y = pos_y
106
+ @matrix = matrix
107
+
108
+ @distance = @window.glass.spaces_below(self)
109
+ end
110
+ end
111
+
112
+ FIGURES = {
113
+ :cross => ' x |xxx | x | |white',
114
+ :stairs => 'x |xx | xx | |yellow',
115
+ :batch => 'x x |xxx | | |purple',
116
+ :l_kuckan => 'xxx | x | x | |yellow',
117
+ :s_kuckan => 'xxx | x | | |yellow',
118
+ :l_corner => 'x |x |xxx | |white',
119
+ :s_corner => 'x |xx | | |white',
120
+ :l_square => 'xx |xx | | |blue',
121
+ :s_square => 'x | | | |blue',
122
+ :l_cap => ' xx |xxx | | |blue',
123
+ :r_cap => 'xx |xxx | | |blue',
124
+ :l_peris => 'xx | x | xx | |green',
125
+ :r_peris => ' xx | x |xx | |purple',
126
+ :l_zigota => 'x |xxx | x | |green',
127
+ :r_zigota => ' x |xxx | x | |cyan',
128
+ :ll_worm => 'xx | xxx | | |green',
129
+ :lr_worm => ' xx |xxx | | |cyan',
130
+ :sl_worm => 'xx | xx | | |green',
131
+ :sr_worm => ' xx |xx | | |cyan',
132
+ :l_crutch => 'x |xx |x |x |white',
133
+ :r_crutch => ' x |xx | x | x |yellow',
134
+ :sl_hook => 'xxx |x | | |cyan',
135
+ :sr_hook => 'x |xxx | | |white',
136
+ :ll_hook => 'xxxx |x | | |cyan',
137
+ :lr_hook => 'x |xxxx | | |yellow',
138
+ :t_stick => 'xx | | | |red',
139
+ :s_stick => 'xxx | | | |red',
140
+ :l_stick => 'xxxx | | | |red',
141
+ :h_stick => 'xxxxx| | | |red'
142
+ }.freeze
143
+
144
+ COLORS = {
145
+ :cyan => 0xFF00F7F1,
146
+ :blue => 0xFF0000FF,
147
+ :orange => 0xFFF69F00,
148
+ :yellow => 0xFFEAF800,
149
+ :green => 0xFF00FF00,
150
+ :purple => 0xFFB200F8,
151
+ :red => 0xFFFE0000,
152
+ :white => 0xFFFFFFFF
153
+ }.freeze
154
+
155
+ end
data/lib/finish.rb ADDED
@@ -0,0 +1,85 @@
1
+ #
2
+ # The Game-Over screen
3
+ #
4
+ # Copyright (C) 2011 Nikolay Nemshilov
5
+ #
6
+ class Finish
7
+ WIDTH = 27
8
+ HEIGHT = 27
9
+
10
+ BG_COLOR = Color::BLACK
11
+
12
+ OVER_FONT = ['Courier', Block::SIZE * 4, Color::RED].freeze
13
+ HEAD_FONT = ['Courier', Block::SIZE + 5, Color::GRAY].freeze
14
+ TEXT_FONT = ['Courier New', Block::SIZE, Color::GRAY].freeze
15
+
16
+ X_OFFSET = 3.5 * Block::SIZE
17
+
18
+ #
19
+ # Basic constructor
20
+ #
21
+ def initialize(window)
22
+ @window = window
23
+ @block = Block.new(window, BG_COLOR)
24
+
25
+ @over_font = Font.new(window, OVER_FONT[0], OVER_FONT[1])
26
+ @head_font = Font.new(window, HEAD_FONT[0], HEAD_FONT[1])
27
+ @text_font = Font.new(window, TEXT_FONT[0], TEXT_FONT[1])
28
+
29
+ @text_input = TextInput.new
30
+
31
+ reset!
32
+ end
33
+
34
+ def draw
35
+ # rendering the black background
36
+ (0..WIDTH).each do |x|
37
+ (0..HEIGHT).each do |y|
38
+ @block.draw(x, y)
39
+ end
40
+ end
41
+
42
+ @over_font.draw("GAME OVAR!",
43
+ X_OFFSET, 2 * Block::SIZE,
44
+ 0, 1.0, 1.0, OVER_FONT[2])
45
+
46
+ @head_font.draw("Hiscores: ",
47
+ X_OFFSET, 7 * Block::SIZE,
48
+ 0, 1.0, 1.0, HEAD_FONT[2])
49
+
50
+ score = @score.to_s
51
+ text = @text_input.text.slice(0, 32) + (@window.text_input == @text_input ? '_' : '')
52
+ @text_font.draw(
53
+ text.ljust(43 - score.size, '.') + score,
54
+ X_OFFSET, 9 * Block::SIZE, 0, 1.0, 1.0, TEXT_FONT[2])
55
+
56
+ @hiscores.each_with_index do |record, i|
57
+ score = record[1].to_s
58
+
59
+ @text_font.draw(
60
+ record[0].ljust(43 - score.size, '.') + score,
61
+ X_OFFSET, (10 + i) * Block::SIZE,
62
+ 0, 1.0, 1.0, TEXT_FONT[2])
63
+ end
64
+ end
65
+
66
+ def score=(score)
67
+ @score = score
68
+ @hiscores = Records.top(14)
69
+ @window.text_input = @text_input
70
+ end
71
+
72
+ def reset!
73
+ @score = 0
74
+ @hiscores = []
75
+ @window.text_input = nil
76
+ end
77
+
78
+ def enter!
79
+ name = @text_input.text.strip
80
+ unless name == ''
81
+ Records.add(name, @score)
82
+ @window.text_input = nil
83
+ end
84
+ end
85
+ end
data/lib/game.rb ADDED
@@ -0,0 +1,104 @@
1
+ #
2
+ # The main game window class
3
+ #
4
+ # NOTE: all the sizes in the code are _in_blocks_
5
+ #
6
+ # Copyright (C) 2011 Nikolay Nemshilov
7
+ #
8
+ class Game < Window
9
+
10
+ attr_accessor :glass, :status
11
+
12
+ def initialize
13
+ super(27 * Block::SIZE, 27 * Block::SIZE, false)
14
+ self.caption = "Pentix"
15
+
16
+ @glass = Glass.new(self, 1, 1)
17
+ @status = Status.new(self, 16, 1)
18
+
19
+ @controls = Controls.new # keybindings
20
+ @finish = Finish.new(self) # the gameover screen
21
+
22
+ reset!
23
+ end
24
+
25
+ def draw
26
+ if @playing
27
+ @glass.draw
28
+ @status.draw
29
+ @figure.draw
30
+ else
31
+ @finish.draw
32
+ end
33
+ end
34
+
35
+ def update
36
+ if @playing && time_to_move
37
+ if @figure.distance > 0
38
+ @figure.move_down
39
+ else
40
+ @figure.drop
41
+ end
42
+ end
43
+ end
44
+
45
+ def reset!
46
+ @status.reset!
47
+ @glass.reset!
48
+ @finish.reset!
49
+
50
+ show_next_figure
51
+
52
+ @time_offset = 1000 / @status.level # blocks per second
53
+ @next_time = 0
54
+
55
+ time_to_move # precalculating the next time to move
56
+
57
+ @playing = true
58
+ end
59
+
60
+ def its_over!
61
+ @finish.score = @status.score
62
+ @playing = false
63
+ end
64
+
65
+ def button_down(button)
66
+ case @controls.command_for(button)
67
+ when :drop then @figure.drop if @playing
68
+ when :left then @figure.move_left if @playing
69
+ when :right then @figure.move_right if @playing
70
+ when :turn_left then @figure.turn_left if @playing
71
+ when :turn_right then @figure.turn_right if @playing
72
+ when :reset then reset!
73
+ when :quit then close
74
+ else
75
+ @finish.enter! if button == Button::KbReturn
76
+ end
77
+ end
78
+
79
+ def show_next_figure
80
+ @figure = @status.figure || Figure.new(self)
81
+ @status.figure = Figure.new(self)
82
+
83
+ x = (Glass::WIDTH - @figure.size_x)/2 + 2
84
+ y = @glass.pos_y
85
+
86
+ if @glass.has_space_for?(@figure.matrix, x, y)
87
+ @figure.move_to(x, y)
88
+ else
89
+ @figure.pos_x = x
90
+ @figure.pos_y = y
91
+
92
+ its_over!
93
+ end
94
+ end
95
+
96
+ def time_to_move
97
+ if Gosu::milliseconds > @next_time
98
+ @next_time = Gosu::milliseconds + @time_offset
99
+ else
100
+ false
101
+ end
102
+ end
103
+
104
+ end
data/lib/glass.rb ADDED
@@ -0,0 +1,158 @@
1
+ #
2
+ # The glass thing. It handles the figure position calculcations
3
+ # watches the available space, removes full lines, etc, etc.
4
+ #
5
+ # Copyright (C) 2011 Nikolay Nemshilov
6
+ #
7
+ class Glass
8
+ attr_accessor :pos_x, :pos_y, :matrix
9
+
10
+ WIDTH = 12
11
+ HEIGHT = 24
12
+ COLOR = Color::GRAY
13
+
14
+ #
15
+ # Basic constructor
16
+ #
17
+ def initialize(window, x, y)
18
+ @window = window
19
+ @block = Block.new(window, COLOR)
20
+
21
+ @pos_x = x
22
+ @pos_y = y
23
+
24
+ reset!
25
+ end
26
+
27
+ #
28
+ # Empties the glass by creating a new blocks matrix
29
+ #
30
+ def reset!
31
+ @matrix = (0..HEIGHT-1).map do
32
+ Array.new(WIDTH)
33
+ end
34
+ end
35
+
36
+ #
37
+ # Draws the class walls and content
38
+ #
39
+ def draw
40
+ @block.color = COLOR
41
+
42
+ # drawing the walls
43
+ (0..HEIGHT).each do |i|
44
+ @block.draw(@pos_x, @pos_y + i)
45
+ @block.draw(@pos_x + WIDTH + 1, @pos_y + i)
46
+ end
47
+
48
+ # drawing the bottom
49
+ (1..WIDTH).each do |i|
50
+ @block.draw(@pos_x + i, @pos_y + HEIGHT)
51
+ end
52
+
53
+ # drawing the blocks inside
54
+ @matrix.each_with_index do |row, y|
55
+ row.each_with_index do |color, x|
56
+ unless color == nil
57
+ @block.color = color
58
+ @block.draw(@pos_x + x + 1, @pos_y + y)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ #
65
+ # Calculates the available space (in blocks)
66
+ # below the figure
67
+ #
68
+ def spaces_below(figure)
69
+ fig_x = figure.pos_x - @pos_x - 1
70
+ fig_y = figure.pos_y - @pos_y
71
+
72
+ (0..figure.size_x-1).map do |x|
73
+ column_height = 0
74
+
75
+ figure.matrix.each_with_index do |row, y|
76
+ column_height = y + 1 if row[x]
77
+ end
78
+
79
+ lowest_point = fig_y + column_height
80
+
81
+ x += fig_x
82
+ distance = HEIGHT - lowest_point
83
+
84
+ # checking if it interescts with any existing blocks
85
+ @matrix.each_with_index do |row, y|
86
+ if nil != row[x]
87
+ distance = y - lowest_point
88
+ break
89
+ end
90
+ end
91
+
92
+ distance
93
+ end.min
94
+ end
95
+
96
+ #
97
+ # Checks if this figure matrix can fit the glass at the given
98
+ # position. Used for prechecks on figure manipulations to
99
+ # enforce movement constraints
100
+ #
101
+ def has_space_for?(matrix, pos_x, pos_y)
102
+ if pos_x > @pos_x && pos_x < (@pos_x + WIDTH + 2 - matrix[0].size)
103
+ if pos_y >= @pos_y && pos_y < (@pos_y + HEIGHT + 1 - matrix.size)
104
+ matrix.each_with_index do |row, y|
105
+ row.each_with_index do |visible, x|
106
+ if visible && nil != @matrix[pos_y - @pos_y + y][pos_x - @pos_x + x - 1]
107
+ return false
108
+ end
109
+ end
110
+ end
111
+
112
+ return true
113
+ end
114
+ end
115
+
116
+ false
117
+ end
118
+
119
+ #
120
+ # Glues the figure into the glass. The figure might hang above
121
+ # or virtually get through the stack, it doesn't matter. This
122
+ # method uses the horizontal position only
123
+ #
124
+ def glue_in(figure)
125
+ @window.status.count_drop(figure)
126
+
127
+ (0..figure.size_x - 1).each do |x|
128
+ (0..figure.size_y-1).each do |y|
129
+ if figure.matrix[y][x]
130
+ @matrix[
131
+ y + figure.pos_y - @pos_y + figure.distance
132
+ ][
133
+ x + figure.pos_x - @pos_x - 1
134
+ ] = figure.color
135
+ end
136
+ end
137
+ end
138
+
139
+ remove_full_lines
140
+ end
141
+
142
+ #
143
+ # Checks the glass for full lines, removes them
144
+ # and refills the glass
145
+ #
146
+ def remove_full_lines
147
+ lines = @matrix.map do |row|
148
+ row.all? ? row : nil
149
+ end.compact!
150
+
151
+ lines.each do |row|
152
+ @matrix.delete(row)
153
+ @matrix.unshift Array.new(WIDTH)
154
+ end
155
+
156
+ @window.status.count_kill(lines)
157
+ end
158
+ end
data/lib/records.rb ADDED
@@ -0,0 +1,40 @@
1
+ #
2
+ # A little wrapper to handle hiscore records
3
+ #
4
+ class Records < Array
5
+ FILENAME = File.join(ENV['HOME'] || ENV['USERPROFILE'], ".pentix")
6
+
7
+ def self.new
8
+ @@instance ||= super # no need to bother HDD all the time
9
+ end
10
+
11
+ def self.top(size)
12
+ new.sort{ |a, b| b[1] <=> a[1] }.uniq.slice(0, size)
13
+ end
14
+
15
+ def self.add(name, score)
16
+ list = self.new
17
+ list << [name.strip, score]
18
+ list.save
19
+ end
20
+
21
+ def initialize(*args)
22
+ super *args
23
+
24
+ File.read(FILENAME).split("\n").each do |line|
25
+ if match = line.match(/^\s*(.*?)\s+(\d+)\s*$/)
26
+ self << [match[1].strip, match[2].to_i]
27
+ end
28
+ end if File.exists?(FILENAME)
29
+ end
30
+
31
+ def save
32
+ File.open(FILENAME, "w") do |file|
33
+ each do |entry|
34
+ if entry[1] > 0
35
+ file.write "#{entry[0].ljust(40, ' ')} #{entry[1]}\n"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end