upwords 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,149 @@
1
+ # require 'colored'
2
+
3
+ module Upwords
4
+ class Graphics < Curses::Window
5
+
6
+ def initialize(game)
7
+ super(0,0,0,0)
8
+ @game = game
9
+ @board = @game.board
10
+ @cursor = @game.cursor
11
+ @message = ''
12
+ @rack_visibility = false
13
+ end
14
+
15
+ def refresh
16
+ clear
17
+ self << self.to_s
18
+ super
19
+ end
20
+
21
+ def to_s
22
+ (draw_board + draw_message).zip(draw_stats).map do |board_row, stats_row|
23
+ (board_row.to_s) + (stats_row.to_s)
24
+ end.join("\n")
25
+ end
26
+
27
+ def message=(new_message)
28
+ if new_message.nil?
29
+ @message = ""
30
+ else
31
+ @message = new_message
32
+ end
33
+ end
34
+
35
+ def show_rack
36
+ @rack_visibility = true
37
+ end
38
+
39
+ def hide_rack
40
+ @rack_visibility = false
41
+ end
42
+
43
+ def toggle_rack_visibility
44
+ @rack_visibility = !@rack_visibility
45
+ end
46
+
47
+ private
48
+
49
+ def format_letter(row, col)
50
+ letter = @board.top_letter(row, col)
51
+ if letter.nil?
52
+ letter = " "
53
+ # add blank space after all other letters except Qu
54
+ elsif letter != "Qu"
55
+ letter += " "
56
+ end
57
+ letter
58
+ end
59
+
60
+ def draw_player_name
61
+ "#{@game.current_player.name}'s turn"
62
+ end
63
+
64
+ def draw_score(player)
65
+ "#{player.name}'s Score: #{player.score}"
66
+ end
67
+
68
+ def draw_last_turn(player)
69
+ "Last Move: #{player.last_turn}"
70
+ end
71
+
72
+ def draw_letter_rack
73
+ "Letters: #{@game.current_player.show_rack(!@rack_visibility)} "
74
+ end
75
+
76
+ def draw_space(row, col, cursor_posn)
77
+ cursor = cursor_posn == [row, col] ? "*" : " "
78
+ "#{cursor}#{format_letter(row, col)} "
79
+ end
80
+
81
+ def draw_row(row, cursor_posn)
82
+ ["|",
83
+ (0...@board.num_columns).map do |col|
84
+ draw_space(row, col, cursor_posn)
85
+ end.join("|"),
86
+ "|"].join
87
+ end
88
+
89
+ # This will also display the stack height for now
90
+ def draw_divider(row, col, show_height=true)
91
+ height = @board.stack_height(row, col)
92
+ if show_height && height > 0
93
+ height = height.to_s
94
+ else
95
+ height = "-"
96
+ end
97
+ "---#{height}"
98
+ end
99
+
100
+ def draw_row_divider(row, show_height=true)
101
+ ["+",
102
+ (0...@board.num_columns).map do |col|
103
+ draw_divider(row, col, show_height)
104
+ end.join("+"),
105
+ "+"].join
106
+ end
107
+
108
+ # print grid of top letter on each stack and stack height
109
+ def draw_board
110
+ b = [draw_row_divider(0, false)]
111
+
112
+ (0...@board.num_rows).each do |i|
113
+ b << draw_row(i, @cursor.posn)
114
+ b << draw_row_divider(i)
115
+ end
116
+
117
+ b.to_a
118
+ end
119
+
120
+ def draw_message
121
+ ["", @message.to_s]
122
+ end
123
+
124
+ def draw_stats
125
+ ["--------------------",
126
+ draw_score(@game.players[0]),
127
+ "",
128
+ draw_letter_rack,
129
+ "--------------------",
130
+ "",
131
+ @game.player_count > 1 ?draw_score(@game.players[1]) : "",
132
+ "",
133
+ @game.player_count > 2 ? draw_score(@game.players[2]) : "",
134
+ "",
135
+ @game.player_count > 3 ? draw_score(@game.players[3]) : "",
136
+ "",
137
+ "----------------------",
138
+ "| Controls |",
139
+ "----------------------",
140
+ "Show Letters [SPACE]",
141
+ "Undo Last Move [DEL]",
142
+ "Submit Move [ENTER]",
143
+ "Swap Letter [+]",
144
+ "Skip Turn [-]",
145
+ "Quit Game [SHIFT+Q]"].map{|s| " #{s}"} # Left padding
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,22 @@
1
+ module Upwords
2
+ class LetterBank
3
+
4
+ def initialize(letters=[])
5
+ @bank = letters.dup
6
+ end
7
+
8
+ def empty?
9
+ @bank.empty?
10
+ end
11
+
12
+ def draw
13
+ unless self.empty?
14
+ @bank.delete_at(rand(@bank.size))
15
+ end
16
+ end
17
+
18
+ def deposit(letter)
19
+ @bank << letter
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ module Upwords
2
+ class LetterRack
3
+
4
+ attr_reader :capacity, :letters
5
+
6
+ def initialize(capacity=7)
7
+ @letters = []
8
+ @capacity = capacity
9
+ end
10
+
11
+ def size
12
+ @letters.size
13
+ end
14
+
15
+ def full?
16
+ size == capacity
17
+ end
18
+
19
+ def empty?
20
+ @letters.empty?
21
+ end
22
+
23
+ def has_letter?(letter)
24
+ @letters.include? letter
25
+ end
26
+
27
+ def add(letter)
28
+ if full?
29
+ raise IllegalMove, "Rack is full!"
30
+ else
31
+ @letters << letter
32
+ end
33
+ end
34
+
35
+ def remove(letter)
36
+ if has_letter?(letter)
37
+ @letters.delete_at(@letters.index(letter))
38
+ else
39
+ raise IllegalMove, "You don't have this letter!"
40
+ end
41
+ end
42
+
43
+ def show
44
+ @letters.join(' ')
45
+ end
46
+
47
+ def show_masked
48
+ @letters.map {'*'}.join(' ')
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,98 @@
1
+ module Upwords
2
+ class Move
3
+
4
+ def initialize(tiles = [])
5
+ @shape = Shape.new(tiles.map {|(row, col), letter| [row, col]})
6
+ @move = tiles.to_h
7
+ end
8
+
9
+ # TODO: remove dict from word class
10
+ # TODO: move score and new word methods to board class?
11
+ def score(board, player)
12
+ new_words(board).reduce(player.rack_capacity == @move.size ? 20 : 0) do |total, word|
13
+ total += word.score
14
+ end
15
+ end
16
+
17
+ # TODO: Add the following legal move checks:
18
+ # - Move is not a simple pluralization? (e.g. Cat -> Cats is NOT a legal move)
19
+ def legal?(board, dict, raise_exception = false)
20
+ legal_shape?(board, raise_exception) && legal_words?(board, dict, raise_exception)
21
+ end
22
+
23
+ def legal_shape?(board, raise_exception = false)
24
+ @shape.legal?(board, raise_exception)
25
+ end
26
+
27
+ def can_play_letters?(board, raise_exception = false)
28
+ @move.all? do |(row, col), letter|
29
+ board.can_play_letter?(letter, row, col, raise_exception)
30
+ end
31
+ end
32
+
33
+ # TODO: Add the following legal move checks:
34
+ # - Move is not a simple pluralization? (e.g. Cat -> Cats is NOT a legal move)
35
+ def legal_words?(board, dict, raise_exception = false)
36
+
37
+ if can_play_letters?(board, raise_exception)
38
+ bad_words = self.new_illegal_words(board, dict)
39
+ if bad_words.empty?
40
+ return true
41
+ else
42
+ raise IllegalMove, "#{bad_words.join(', ')} #{bad_words.size==1 ? 'is not a legal word' : 'are not legal words'}!" if raise_exception
43
+ end
44
+ end
45
+
46
+ return false
47
+ end
48
+
49
+ def position?(row, col)
50
+ @move.key?([row, col])
51
+ end
52
+
53
+ def [](row, col)
54
+ @move[[row, col]]
55
+ end
56
+
57
+ def play(board)
58
+ @move.reduce(board) do |b, (posn, letter)|
59
+ b.play_letter(letter, *posn)
60
+ b
61
+ end
62
+ end
63
+
64
+ # TODO: consider move main subroutine to Shape class?
65
+ def remove_from(board)
66
+ if @move.any? {|(row, col), letter| board.top_letter(row, col) != letter}
67
+ raise IllegalMove, "Move does not exist on board and therefore cannot be removed!"
68
+ else
69
+ (@move.each_key).reduce(board) do |b, posn|
70
+ b.remove_top_letter(*posn)
71
+ b
72
+ end
73
+ end
74
+ end
75
+
76
+ # TODO: handle exceptions when board cannot be updated with new move
77
+ # TODO: move score and new word methods to board class?
78
+ def new_words(board)
79
+ # HACK: update board with new move
80
+ words = (board.play_move(self).word_positions).select do |word_posns|
81
+ word_posns.any? {|row, col| position?(row, col)}
82
+
83
+ end.map do |word_posns|
84
+ Word.new(word_posns, board)
85
+ end
86
+
87
+ # HACK: remove move from board
88
+ remove_from(board)
89
+
90
+ words
91
+ end
92
+
93
+ def new_illegal_words(board, dict)
94
+ new_words(board).reject {|word| dict.legal_word?(word.to_s)}
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,64 @@
1
+ module Upwords
2
+ class MoveManager
3
+
4
+ def initialize(board, dictionary)
5
+ @board = board
6
+ @dict = dictionary
7
+ @pending_move = []
8
+ @move_history = [] # TODO: Add filled board spaces as first move if board is not empty
9
+ end
10
+
11
+ # --------------------------------
12
+ # Player-Board Interaction Methods
13
+ # --------------------------------
14
+ def add(player, letter, row, col)
15
+ # TODO: remove the need for @pending_move.map
16
+ if (@pending_move.map {|m| m[0]}).include?([row, col])
17
+ raise IllegalMove, "You can't stack on a space more than once in a single turn!"
18
+ elsif
19
+ @pending_move << player.play_letter(@board, letter, row, col)
20
+ end
21
+ end
22
+
23
+ def undo_last(player)
24
+ if @pending_move.empty?
25
+ raise IllegalMove, "No moves to undo!"
26
+ else
27
+ player.take_from(@board, *@pending_move.pop[0]) # TODO: make Tile class
28
+ end
29
+ end
30
+
31
+ def undo_all(player)
32
+ until @pending_move.empty? do
33
+ undo_last(player)
34
+ end
35
+ end
36
+
37
+ def submit(player)
38
+ if @pending_move.empty?
39
+ raise IllegalMove, "You haven't played any letters!"
40
+ elsif legal?
41
+ player.score += pending_score(player)
42
+ @move_history << Move.new(@pending_move)
43
+ @pending_move.clear
44
+ end
45
+ end
46
+
47
+ # TODO: cache prev board in local variable...
48
+ def pending_words
49
+ prev_board = Board.build(@move_history, @board.size, @board.max_height)
50
+ Move.new(@pending_move).new_words(prev_board)
51
+ end
52
+
53
+ def pending_score(player)
54
+ prev_board = Board.build(@move_history, @board.size, @board.max_height)
55
+ Move.new(@pending_move).score(prev_board, player)
56
+ end
57
+
58
+ def legal?
59
+ prev_board = Board.build(@move_history, @board.size, @board.max_height)
60
+ Move.new(@pending_move).legal?(prev_board, @dict, raise_exception = true)
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,159 @@
1
+ module Upwords
2
+ class Player
3
+
4
+ attr_reader :name, :cpu
5
+ attr_accessor :score, :last_turn
6
+
7
+ def initialize(name, rack_capacity=7, cpu=false)
8
+ @name = name
9
+ @rack = LetterRack.new(rack_capacity)
10
+ @score = 0
11
+ @last_turn = nil
12
+ @cpu = cpu
13
+ end
14
+
15
+ def cpu?
16
+ @cpu
17
+ end
18
+
19
+ def letters
20
+ @rack.letters.dup
21
+ end
22
+
23
+ def show_rack(masked = false)
24
+ masked ? @rack.show_masked : @rack.show
25
+ end
26
+
27
+ def rack_full?
28
+ @rack.full?
29
+ end
30
+
31
+ def rack_empty?
32
+ @rack.empty?
33
+ end
34
+
35
+ def rack_capacity
36
+ @rack.capacity
37
+ end
38
+
39
+ def take_letter(letter)
40
+ @rack.add(letter)
41
+ end
42
+
43
+ def take_from(board, row, col)
44
+ if board.stack_height(row, col) == 0
45
+ raise IllegalMove, "No letters in #{row}, #{col}!"
46
+ else
47
+ take_letter(board.remove_top_letter(row, col))
48
+ end
49
+ end
50
+
51
+ def play_letter(board, letter, row, col)
52
+ rack_letter = @rack.remove(letter)
53
+ begin
54
+ board.play_letter(rack_letter, row, col)
55
+ rescue IllegalMove => exn
56
+ take_letter(rack_letter)
57
+ raise IllegalMove, exn
58
+ end
59
+ end
60
+
61
+ def swap_letter(letter, letter_bank)
62
+ if letter_bank.empty?
63
+ raise IllegalMove, "Letter bank is empty!"
64
+ else
65
+ trade_letter = @rack.remove(letter)
66
+ take_letter(letter_bank.draw)
67
+ letter_bank.deposit(trade_letter)
68
+ end
69
+ end
70
+
71
+ def refill_rack(letter_bank)
72
+ until rack_full? || letter_bank.empty? do
73
+ take_letter(letter_bank.draw)
74
+ end
75
+ end
76
+
77
+ # ----------------
78
+ # CPU Move Methods
79
+ # ----------------
80
+
81
+ def straight_moves(board)
82
+ rows = board.num_rows
83
+ cols = board.num_columns
84
+
85
+ # Get single-position moves
86
+ one_space_moves = board.coordinates.map {|posn| [posn]}
87
+
88
+ # Get board positions grouped by rows
89
+ (0...rows).map do |row|
90
+ (0...cols).map {|col| [row, col]}
91
+
92
+ # Get horizontal multi-position moves
93
+ end.flat_map do |posns|
94
+ (2..(letters.size)).flat_map {|sz| posns.combination(sz).to_a}
95
+
96
+ # Collect all possible straight moves
97
+ end.reduce(one_space_moves) do |all_moves, move|
98
+ all_moves << move << move.map {|posn| posn.rotate}
99
+ end
100
+ end
101
+
102
+ # TODO: Strip out move filters and have the client provide them in a block
103
+ def legal_move_shapes(board, &filter)
104
+ straight_moves(board).select(&filter)
105
+ end
106
+
107
+ def standard_legal_shape_filter(board)
108
+ proc do |move_arr|
109
+ Shape.new(move_arr).legal?(board)
110
+ end
111
+ end
112
+
113
+ def legal_shape_letter_permutations(board, &filter)
114
+ # Cache result of letter permutation computation for each move size
115
+ letter_perms = Hash.new {|ps, sz| ps[sz] = letters.permutation(sz).to_a}
116
+
117
+ legal_move_shapes(board, &filter).reduce([]) do |all_moves, move|
118
+ letter_perms[move.size].reduce(all_moves) do |move_perms, perm|
119
+ move_perms << move.zip(perm)
120
+ end
121
+ end
122
+ end
123
+
124
+ def cpu_move(board, dict, sample_size = 1000, min_score = 0)
125
+ all_possible_moves = (self.legal_shape_letter_permutations(board, &self.standard_legal_shape_filter(board)))
126
+ all_possible_moves.shuffle!
127
+
128
+ top_score = 0
129
+ top_score_move = nil
130
+
131
+ # TODO: write test for this method
132
+ while top_score_move.nil? || (top_score < min_score) do
133
+
134
+ ([sample_size, all_possible_moves.size].min).times do
135
+
136
+ move_arr = all_possible_moves.pop
137
+ move = Move.new(move_arr)
138
+
139
+ if move.legal_words?(board, dict)
140
+
141
+ move_score = move.score(board, self)
142
+
143
+ if move_score >= top_score
144
+ top_score = move_score
145
+ top_score_move = move_arr
146
+ end
147
+
148
+ end
149
+ end
150
+
151
+ # Decrement minimum required score after each cycle to help prevent long searches
152
+ min_score = [(min_score - 1), 0].max
153
+ end
154
+
155
+ top_score_move
156
+ end
157
+
158
+ end
159
+ end