Perfect Unscrambler WebSocket Demo 简体中文
This project demonstrates an in-memory server to unscramble English word puzzles based on Perfect HTTP Server.
It's a twin project of Perfect Unscrambler, however, instead of jQuery & ReactJS, it is using WebSocket to perform realtime messaging.
Ensure you have installed and activated the latest Swift 3.1 tool chain.
Follow the bash commands below to download, build and run this project:
$ git clone https://github.com/PerfectExamples/Perfect-Unscrambler-WebSocket.git
$ cd Perfect-Unscrambler-WebSocket
$ swift build
$ .build/debug/Unscrambler
It will need a few seconds to download the online dictionary text and do some indexing, so please wait until the following messages appeared in your terminal:
[BOOT] 354983 words are loaded into 311906 groups.
[INFO] Starting HTTP server localhost on 0.0.0.0:8888
This means you can use browser to check http://localhost:8888
now:
If nothing wrong, you can type any words into the input box and shall see solutions immediately.
To stop the server, simply go back to the terminal and type CTRL-C
.
-
No Database Required: Perfect Unscrambler uses Swift
Dictionary
as the primary data structure to solve the input puzzle other than depending on any databases. -
In-Memory & Indexing: In this example, a frequency table has been applied to solve the incoming puzzle as the key of indexing. All operations are taking in memory which maximizes the server performance.
-
WebSocket Demo: This demo also shows how WebSocket works with Perfect Server.
This example server depends on an open sourced web link below, and you can modify or replace them in the source code main.swift
if broken:
/// any valid word list file given each word per line.
let dictionaryURL = "https://raw.githubusercontent.com/dwyl/english-words/master/words.txt"
LICENSE & COPYRIGHT
Please note that the web link above are not a part of this project (Perfect Unscrambler) and is also subject to the software owner's copyright and license.
Please also check the attached license for more information.
The key to solve the scramble puzzle is using an alphabetic frequency table to compare the puzzle and corresponding candidates, as picture below:
As you can see, we can write down the frequency number of each letter in a word and use this information to look solutions in the dictionary, which means any word with the same frequency table will be a valid solution.
In the picture above, word "TEAM" can be summarized as one "A", one "E", one "M" and one "T", so we can make a note for this word as A1E1M1T1
if put everything in order. Here we name this note as the signature of a word.
Then imagine that given a dictionary in hand, we can pick out the word from the dictionary one by one, and redo the same frequency calculation to see if the solution will pop out when both words share the same frequency signature. In the picture example, four different solutions - "MATE", "MEAT", "META" and "TAME" - are found for sharing the same key "A1E1M1T1" with "TEAM".
Of course up to now, we can calculate the user input and check the whole dictionary during the server request. However, going over a large dictionary is always a time consuming process. To avoid unnecessary real time iteration, a prebuilt indexing procedure before accepting any incoming requests - which means to calculate the frequency signatures of each word after loading the dictionary and then preserve these signatures in memory - is a good idea.
In this case, a WordGroups
class is well designed to store the whole well-indexed dictionary. Core storage of WordGroups
is a Swift Dictionary<key, value>
structure, i.e, the variable type of [String:[String]]
, where as the signature is the key and the value is a group of words which share the same signature.
So here are the server booting procedures:
- download dictionary - about 350,000+ words in our example.
- perform indexing over the words - the words will be divided into 300,000+ groups, which means about 50,000 words have multiple solutions to specific scrambles.
- start the HTTP server.
Once successfully booted, the server should respond to any request in an amazing speed - calculate the signature, retrieve the solution array pick out the group and send it back, or return a blank array to represent "nothing found".
The whole project only contains three Swift source files.
It is a common file required by Swift Package Manager, with key lines of components for the server:
dependencies: [
.Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2),
.Package(url: "https://github.com/PerfectlySoft/Perfect-WebSockets.git", majorVersion:2),
.Package(url: "https://github.com/PerfectlySoft/Perfect-CURL.git", majorVersion:2)
]
Among these dependencies, Perfect-HTTPServer includes all essential functions of a Swift HTTP Server on both mac / linux; Perfect-CURL is a library that enables the Swift code to load dynamic data from other web api services; Besides, Perfect-WebSockets provides WebSocket access to clients.
WordGroups class has only three functions:
func signature()
: This is the key of whole algorithm, which summaries a word to a string to represent the corresponding frequency table of the input word.
// prepare an empty frequency table
var table:[UInt8: Int] = [:]
// get the lowercased string
word.lowercased().utf8
// remove all non-alphabetic characters, numbers and symbols
.filter {$0 > 96 && $0 < 123}
// build the frequency table
.forEach { char in
// get the current frequency of the objective character
let f = table[char] ?? 0
// increase one for the current
table[char] = f + 1
}//next
// must sort
return table.keys.sorted()
// map it to strings like `a1`
.map { key -> String in
let buffer:[UInt8] = [key, 0]
return String(cString: buffer) + String(describing: table[key] ?? 0)
}
// finally, join all key-frequency pair
.joined()
init()
: The constructor ofWordGroups
class, it will download a dictionary from the url, which is a long wordlist and each line of the list is a unique word. Once downloaded, it will automatically divid the whole list into different groups, each group will marked with a unique signature calculated from thesignature()
function.
// download the online dictionary
let curl = CURL(url: url)
let r = curl.performFullySync()
curl.close()
// split the dictionary into lines
r.bodyBytes.split(separator: 10).forEach {
// turn the buffer into a string
var s = Array($0)
s.append(0)
let word = String(cString: s)
// calculate the signature
let sig = signature(word: word)
// get the group by signature
var list = groups[sig] ?? []
// add the new word into group
list.append(word)
groups[sig] = list
}//next
func solve()
: Users can input any scramble into this function and it will calculate the signature of the input word as the other did in the loading stage. Then it will look up the existing groups for the key signature and would return the group if found.
// calculate the key
let key = signature(word: scramble)
// retrieve the group from groups
return groups[key] ?? []
main.swift
is the main entry of the server, which is a typical PerfectTemplate application. The server will provide only two routes:
/puzzle
- the WebSocket handler, i.e., incoming WebSocket requests will be processed as program below:
public class PuzzleHandler: WebSocketSessionHandler {
public let socketProtocol : String? = "puzzle"
// This function is called by the WebSocketHandler once the connection has been established.
public func handleSession(request: HTTPRequest, socket: WebSocket) {
socket.readStringMessage { raw, _, _ in
guard let raw = raw else {
// This block will be executed if, for example, the browser window is closed.
socket.close()
return
}//end guard
// get input from the client
let string = sanitizedInput(raw)
// send back the solution
let solution = words.solve(scramble: string).map { "\($0)\n" }.joined()
socket.sendStringMessage(string: solution, final: true) {
self.handleSession(request: request, socket: socket)
}//end send
}//end readStringMessage
}//end handleSession
}//end Puzzle
/
- the root handler, i.e., a static HTML home page, but client user behaviour - type any puzzle into the input box and immediately pop out all possible unscramble solutions - is controlled by a small piece of WebSocket script embedded in the HTML:
var input, output;
function init(){
input = document.getElementById('textInput');
output = document.getElementById('results');
sock = new WebSocket('ws://' + window.location.host + '/puzzle', 'puzzle');
sock.onmessage = function(evt) { output.innerText = evt.data; }
}//end init
function send() {
sock.send(input.value);
}
We are transitioning to using JIRA for all bugs and support related issues, therefore the GitHub issues has been disabled.
If you find a mistake, bug, or any other helpful suggestion you'd like to make on the docs please head over to http://jira.perfect.org:8080/servicedesk/customer/portal/1 and raise it.
A comprehensive list of open issues can be found at http://jira.perfect.org:8080/projects/ISS/issues
For more information on the Perfect project, please visit perfect.org.