upwords 0.1.1

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.
@@ -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