pentix 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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