1 /***********************************************************************
2  *
3  * Copyright (C) 2009, 2013, 2014 Graeme Gott <graeme@gottcode.org>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  ***********************************************************************/
19 
20 #include "word.h"
21 
22 #include "board.h"
23 #include "cell.h"
24 #include "letter.h"
25 #include "wordlist.h"
26 
27 #include <QGraphicsPathItem>
28 #include <QPainterPath>
29 
30 #include <algorithm>
31 
factorial(int num)32 static int factorial(int num) {
33 	return (num > 1) ? (factorial(num - 1) * num) : 1;
34 }
35 
countPermutations(const QString & word)36 static int countPermutations(const QString& word) {
37 	Q_ASSERT(!word.isEmpty());
38 
39 	int n = factorial(word.length());
40 	int d = 1;
41 
42 	QString sorted = word;
43 	std::sort(sorted.begin(), sorted.end());
44 	QChar c;
45 	int count = 0;
46 	for (const QChar& c2 : sorted) {
47 		if (c2 != c) {
48 			c = c2;
49 			d *= factorial(count);
50 			count = 1;
51 		} else {
52 			count++;
53 		}
54 	}
55 	d *= factorial(count);
56 
57 	return n / d;
58 }
59 
60 //-----------------------------------------------------------------------------
61 
Word(const QString & word,const QPoint & position,Qt::Orientation orientation,std::mt19937 & random)62 Word::Word(const QString& word, const QPoint& position, Qt::Orientation orientation, std::mt19937& random)
63 : m_board(0), m_correct(false), m_orientation(orientation), m_random(random) {
64 	if (word.isEmpty()) {
65 		return;
66 	}
67 	m_solutions.append(word);
68 
69 	QPoint delta = (orientation == Qt::Horizontal) ? QPoint(1, 0) : QPoint(0, 1);
70 	QPoint pos = position;
71 	for (int i = 0; i < word.length(); ++i) {
72 		m_positions.append(pos);
73 		pos += delta;
74 	}
75 }
76 
77 //-----------------------------------------------------------------------------
78 
click()79 void Word::click() {
80 	m_board->click(toString());
81 }
82 
83 //-----------------------------------------------------------------------------
84 
check()85 void Word::check() {
86 	QString word;
87 	for (const QPoint& pos : m_positions) {
88 		word += m_board->cell(pos.x(), pos.y())->letter()->character();
89 	}
90 	if (m_solutions.contains(word)) {
91 		m_correct = true;
92 		for (const QPoint& pos : m_positions) {
93 			Cell* cell = m_board->cell(pos.x(), pos.y());
94 			cell->setWord(this);
95 			cell->letter()->setCorrect();
96 		}
97 		m_board->setCurrentWord(0);
98 		m_board->check(m_solutions.at(0), word);
99 	}
100 }
101 
102 //-----------------------------------------------------------------------------
103 
hint()104 QGraphicsItem* Word::hint() {
105 	if (isCorrect()) {
106 		return 0;
107 	}
108 
109 	// Find position of first incorrect character
110 	int count = m_positions.count();
111 	int pos = -1;
112 	QString solution;
113 	for (const QString& word : m_solutions) {
114 		for (int i = 0; i < count; ++i) {
115 			const QPoint& point = m_positions.at(i);
116 			Letter* letter = m_board->cell(point.x(), point.y())->letter();
117 			if (letter->character() != word.at(i)) {
118 				if (i > pos) {
119 					pos = i;
120 					solution = word;
121 				}
122 				break;
123 			}
124 		}
125 	}
126 
127 	// Find letter from end of string which goes in that position
128 	QChar c = solution.at(pos);
129 	int pos2;
130 	QPointF position;
131 	for (pos2 = count - 1; pos2 > pos; --pos2) {
132 		const QPoint& point = m_positions.at(pos2);
133 		Letter* letter = m_board->cell(point.x(), point.y())->letter();
134 		if (letter->isMovable() && letter->character() == c) {
135 			position = letter->scenePos();
136 			letter->setBrush(QColor("#ffbf00"));
137 			break;
138 		}
139 	}
140 
141 	// Create hint graphicsitem
142 	int edge = ((pos2 - pos - 1) * 34) + 16;
143 	QList<QPointF> positions;
144 	positions += QPointF(0, 8);
145 	positions += QPointF(12, 0);
146 	positions += QPointF(12, 5);
147 	positions += QPointF(edge, 5);
148 	positions += QPointF(edge, 11);
149 	positions += QPointF(12, 11);
150 	positions += QPointF(12, 16);
151 	positions += QPointF(0, 8);
152 	QPointF delta = QPointF(-edge, 8);
153 	if (m_orientation == Qt::Vertical) {
154 		delta = QPointF(8, -edge);
155 		for (int i = 0; i < 8; ++i) {
156 			QPointF& pos = positions[i];
157 			qSwap(pos.rx(), pos.ry());
158 		}
159 	}
160 	QPainterPath path;
161 	path.moveTo(positions.at(0));
162 	for (int i = 1; i < 8; ++i) {
163 		path.lineTo(positions.at(i));
164 	}
165 
166 	QGraphicsPathItem* item = m_board->addPath(path, Qt::NoPen, QColor("#ffbf00"));
167 	item->setPos(position + delta);
168 	item->setZValue(20);
169 
170 	return item;
171 }
172 
173 //-----------------------------------------------------------------------------
174 
moveBy(const QPoint & delta)175 void Word::moveBy(const QPoint& delta) {
176 	int count = m_positions.count();
177 	for (int i = 0; i < count; ++i) {
178 		m_positions[i] += delta;
179 	}
180 }
181 
182 //-----------------------------------------------------------------------------
183 
fromString(const QString & shuffled)184 void Word::fromString(const QString& shuffled) {
185 	// Find movable letters
186 	QString sorted1;
187 	QList<Letter*> letters;
188 	for (int i = 0; i < m_positions.count(); ++i) {
189 		const QPoint& pos = m_positions.at(i);
190 		Letter* letter = m_board->cell(pos.x(), pos.y())->letter();
191 		QChar c = letter->character();
192 		sorted1.append(c);
193 		if (letter->isMovable()) {
194 			letters.append(letter);
195 		} else if (c != shuffled.at(i)) {
196 			return;
197 		}
198 	}
199 
200 	// Check if shuffled has the right characters
201 	QString sorted2 = shuffled;
202 	std::sort(sorted1.begin(), sorted1.end());
203 	std::sort(sorted2.begin(), sorted2.end());
204 	if (sorted1 != sorted2) {
205 		return;
206 	}
207 
208 	// Move letters to match shuffled
209 	for (int i = 0; i < m_positions.count(); ++i) {
210 		const QPoint& pos = m_positions.at(i);
211 		Cell* cell = m_board->cell(pos.x(), pos.y());
212 		if (!cell->letter()->isMovable()) {
213 			continue;
214 		}
215 
216 		QChar c = shuffled.at(i);
217 		for (int j = 0; j < letters.count(); ++j) {
218 			if (letters.at(j)->character() == c) {
219 				cell->setLetter(letters.takeAt(j));
220 				break;
221 			}
222 		}
223 	}
224 
225 	check();
226 }
227 
228 //-----------------------------------------------------------------------------
229 
toString() const230 QString Word::toString() const {
231 	QString result;
232 	for (const QPoint& pos : m_positions) {
233 		result.append(m_board->cell(pos.x(), pos.y())->letter()->character());
234 	}
235 	return result;
236 }
237 
238 //-----------------------------------------------------------------------------
239 
setHighlight(bool highlight)240 void Word::setHighlight(bool highlight) {
241 	for (const QPoint& pos : m_positions) {
242 		m_board->cell(pos.x(), pos.y())->letter()->setHighlight(highlight);
243 	}
244 }
245 
246 //-----------------------------------------------------------------------------
247 
shuffle(const WordList * words)248 void Word::shuffle(const WordList* words) {
249 	// Create list of characters and filter the list of words
250 	QString chars;
251 	QString movable;
252 	QString filter;
253 	for (int i = 0; i < m_positions.count(); ++i) {
254 		const QPoint& pos = m_positions.at(i);
255 		Letter* letter = m_board->cell(pos.x(), pos.y())->letter();
256 		QChar c = letter->character();
257 		chars.append(c);
258 		if (letter->isMovable()) {
259 			movable.append(c);
260 			filter.append('.');
261 		} else {
262 			filter.append(c);
263 		}
264 	}
265 	std::sort(chars.begin(), chars.end());
266 	std::sort(movable.begin(), movable.end());
267 	QStringList filtered = words->filter(filter);
268 
269 	// Find valid solutions
270 	for (const QString& valid : filtered) {
271 		QString sorted = valid;
272 		std::sort(sorted.begin(), sorted.end());
273 		if (sorted == chars && !m_solutions.contains(valid)) {
274 			m_solutions.append(valid);
275 		}
276 	}
277 
278 	// Find permutation
279 	QString permuted;
280 	if (m_solutions.count() < countPermutations(movable)) {
281 		do {
282 			std::shuffle(movable.begin(), movable.end(), m_random);
283 			permuted = movable;
284 			for (int i = 0, end = filter.length(); i < end; ++i) {
285 				if (filter.at(i) == '.') {
286 					continue;
287 				}
288 				permuted.insert(i, filter.at(i));
289 			}
290 		} while (m_solutions.contains(permuted));
291 	} else {
292 		permuted = m_solutions.at(0);
293 	}
294 	fromString(permuted);
295 }
296