Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created June 3, 2017 15:56
Show Gist options
  • Save bennadel/3decb897242cfd9d81610f2b9dc0d3a5 to your computer and use it in GitHub Desktop.
Save bennadel/3decb897242cfd9d81610f2b9dc0d3a5 to your computer and use it in GitHub Desktop.
Attempted Regular Expression Pattern Search Game For RegEx Day 2017 Using Angular 4.1.3
// Import the core angular services.
import { Component } from "@angular/core";
import _ = require( "lodash" );
// Import the application services.
import { GridSelection } from "./grid.component";
import { GridSelectionEvent } from "./grid.component";
interface Game {
letters: string[][];
words: string[];
patterns: RegExp[];
}
interface GameItem {
word: string;
pattern: RegExp;
selection: GridSelection;
}
@Component({
selector: "my-app",
styleUrls: [ "./app.component.css" ],
template:
`
<re-grid
[letters]="letters"
[selections]="selections"
(selection)="handleSelection( $event )">
</re-grid>
<ul>
<li *ngFor="let item of items" [class.found]="item.selection">
<strong>{{ item.pattern.source }}</strong>
<span *ngIf="item.selection" (click)="removeSelection( item )" class="remove">
Remove
</span>
</li>
</ul>
`
})
export class AppComponent {
public items: GameItem[];
public letters: string[][];
public selections: GridSelection[];
// I initialize the app component.
constructor() {
var game = this.getGame();
this.letters = game.letters;
this.selections = [];
this.items = game.words.map(
function( word: string, i: number ) : GameItem {
return({
word: word,
pattern: game.patterns[ i ],
selection: null
});
}
);
}
// I check to see if game has been won. And, if so, alerts the user.
public checkStatus() : void {
// The game is considered complete / won if every item is associated with a
// selection on the grid.
var isWinner = this.items.every(
( item: GameItem ) : boolean => {
return( !! item.selection );
}
);
if ( isWinner ) {
setTimeout(
function() {
alert( "Noice! Way to RegExp like a boss!" );
},
500
);
}
}
// I handle the selection event from the grid.
public handleSelection( event: GridSelectionEvent ) : void {
// Since words may be placed on the grid in any direction, we have to check
// the given selection using both the forwards and reversed letters.
var selectedLetters = event.letters.join( "" ).toLowerCase();
var selectedLettersInverse = event.letters.reverse().join( "" ).toLowerCase(); // CAUTION: In-place reverse.
// Check the selection against the game configuration.
for ( var item of this.items ) {
if ( item.selection ) {
continue;
}
if (
( item.word === selectedLetters ) ||
( item.word === selectedLettersInverse )
) {
this.selections.push( item.selection = event.selection );
this.checkStatus();
return;
}
}
}
// I remove the selection associated with the given item.
public removeSelection( item: GameItem ) : void {
this.selections = _.without( this.selections, item.selection );
item.selection = null;
}
// ---
// PRIVATE METHODS.
// ---
// I return a random game configuration.
private getGame() : Game {
// The various board configurations have been generated using the following list
// of words. And, we know that these words map to specific Regular Expression
// patterns (which is what we'll display to the user).
var patternsMap = {
"programmer": /program+er/,
"javascript": /.{4}script/,
"oop": /..p/,
"function": /f.{4}ion/,
"closure": /clos.*?e/,
"ecmascript": /emca.+?t/,
"noop": /n(.)\1p/,
"array": /(a)(r)\2\1y/,
"lexical": /(.)exica\1/,
"prototype": /pr(ot)+?ype/,
"constructor": /con.{5}tor/,
"boolean": /.oo.ean/,
"truthy": /...thy/,
"falsey": /false[aeiouy]/,
"comment": /co(.)\1ent/,
"variable": /var.{5}/,
"method": /.etho./
};
var configurations = [
{
letters: [
"XVFUNCTION".split( "" ),
"TPIRCSAVAJ".split( "" ),
"IYPLLXQYYC".split( "" ),
"LAOHORKEHO".split( "" ),
"AROHSMUSTM".split( "" ),
"CRNMUQYLUM".split( "" ),
"IAOORFEARE".split( "" ),
"XSOYEYKFTN".split( "" ),
"EKPDOHTEMT".split( "" ),
"LEPYTOTORP".split( "" )
],
words: [ "array", "closure", "comment", "falsey", "function", "javascript", "lexical", "method", "noop", "oop", "prototype", "truthy" ]
},
{
letters: [
"DTRNAELOOB".split( "" ),
"DPEHOYTYRZ".split( "" ),
"OIMCVNREPP".split( "" ),
"HRMEAOUSDO".split( "" ),
"TCARRITLTO".split( "" ),
"ESRUITHANN".split( "" ),
"MAGSACYFNS".split( "" ),
"YVOOBNGHPF".split( "" ),
"ZARLLUZOAE".split( "" ),
"UJPCEFOFWP".split( "" )
],
words: [ "array", "boolean", "closure", "falsey", "function", "javascript", "method", "noop", "oop", "programmer", "truthy", "variable" ]
},
{
letters: [
"EEFUNCTION".split( "" ),
"BLUSMETHOD".split( "" ),
"HIBNAELOOB".split( "" ),
"SWPAIAAPEY".split( "" ),
"XLEXICALRH".split( "" ),
"ARRAYRYXUT".split( "" ),
"PJYESLAFSU".split( "" ),
"WSGPOONVOR".split( "" ),
"YGPMSPOOLT".split( "" ),
"ZJTNEMMOCA".split( "" )
],
words: [ "array", "boolean", "closure", "comment", "falsey", "function", "lexical", "method", "noop", "oop", "truthy", "variable" ]
}
];
var selectedConfig = this.getRandom( configurations );
return({
letters: selectedConfig.letters,
words: selectedConfig.words,
// Once we've selected the random game configuration, we have to generate the
// patterns collection based on the words collection. After all, we want the
// users to have to work backwards a bit (from pattern to word to selection).
patterns: selectedConfig.words.map(
( word: string ) : RegExp => {
return( patternsMap[ word ] );
}
)
});
}
// I get a random item from the given collection.
private getRandom<T>( collection: T[] ) : T {
var randomIndex = _.random( collection.length - 1 );
return( collection[ randomIndex ] );
}
}
// Import the core angular services.
import { ChangeDetectionStrategy } from "@angular/core";
import { Component } from "@angular/core";
import { EventEmitter } from "@angular/core";
interface GridLocation {
row: number;
column: number;
}
export interface GridSelectionEvent {
letters: string[];
selection: GridSelection;
}
export interface GridSelectionMapFunction {
( row: number, column: number ) : any;
}
@Component({
selector: "re-grid",
inputs: [ "letters", "selections" ],
outputs: [ "selectionEvent: selection" ],
host: {
"(document: mouseup)": "endSelection()"
},
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: [ "./grid.component.css" ],
templateUrl: "./grid.component.htm"
})
export class GridComponent {
public letters: string[][];
public selectionEvent: EventEmitter<GridSelectionEvent>;
public selections: GridSelection[];
private pendingSelection: GridSelection;
// I initialize the grid component.
constructor() {
this.letters = [];
this.selections = [];
this.selectionEvent = new EventEmitter();
this.pendingSelection = null;
}
// ---
// PUBLIC METHODS.
// ---
// I handle the end of the selection gesture, possibly emitting a new selection if
// the current selection does not conflict with selections that have already been
// placed on the grid.
public endSelection() : void {
if ( ! this.pendingSelection ) {
return;
}
// Check to see if the current selection is wholly contained (ie, subsumed) by
// any of the existing selections.
var isSubsumed = this.selections.some(
( selection: GridSelection ) : boolean => {
return( this.pendingSelection.isSubsumedBy( selection ) );
}
);
// Only emit a selection event if the selection is new.
if ( ! isSubsumed ) {
var selectedLetters = this.pendingSelection.map<string>(
( row: number, column: number ) : string => {
return( this.letters[ row ][ column ] );
}
);
this.selectionEvent.emit({
letters: selectedLetters,
selection: this.pendingSelection
});
}
this.pendingSelection = null;
}
// I check to see if the given grid coordinates are part of a pending selection.
public isPending( row: number, column: number ) : boolean {
if ( ! this.pendingSelection ) {
return( false );
}
return( this.pendingSelection.includes({ row, column }) );
}
// I check to see if the given grid coordinates are part of an existing selection.
public isSelected( row: number, column: number ) : boolean {
var result = this.selections.some(
( selection: GridSelection ) : boolean => {
return( selection.includes({ row, column }) );
}
);
return( result );
}
// I start a new pending selection on the grid.
public startSelection( row: number, column: number ) : void {
this.pendingSelection = new GridSelection({ row, column });
}
// I update the pending selection using the given grid coordinates.
public updateSelection( row: number, column: number ) : void {
if ( ! this.pendingSelection ) {
return;
}
this.pendingSelection.update({ row, column });
}
}
export class GridSelection {
private from: GridLocation;
private to: GridLocation;
// I initialize the grid location with the given starting location.
constructor( start: GridLocation ) {
this.setFrom( start );
}
// ---
// PUBLIC METHODS.
// ---
// I check to see if the given grid location is contained within the selection.
public includes( location: GridLocation ) : boolean {
var isFound = this.gatherLocations().some(
( selectionLocation: GridLocation ) : boolean => {
return(
( location.row === selectionLocation.row ) &&
( location.column === selectionLocation.column )
);
}
);
return( isFound );
}
// I check to see if the current selection completely subsumes the given selection.
public isSubsumedBy( selection: GridSelection ) : boolean {
var isConflict = this.gatherLocations().every(
( location: GridLocation ) : boolean => {
return( selection.includes( location ) );
}
);
return( isConflict );
}
// I map the selected grid location using the given callback / operator.
public map<T>( callback: GridSelectionMapFunction ) : T[] {
var result = this.gatherLocations().map(
( location: GridLocation ) : T => {
return( callback( location.row, location.column ) );
}
);
return( result );
}
// I update the selection using the (TO) grid location.
// --
// CAUTION: This uses a very strict diagonal selection since using a fuzzy diagonal
// runs the risk of moving off the grid and the selection is not aware of the grid
// dimensions. We could probably rework the selection to either know about the gird;
// or, move the selection logic into the grid. But, ... meh.
public update( newTo: GridLocation ) : void {
var deltaRow = Math.abs( newTo.row - this.from.row );
var deltaColumn = Math.abs( newTo.column - this.from.column );
var maxDelta = Math.max( deltaRow, deltaColumn );
// Use the diagonal selection.
if ( deltaRow === deltaColumn ) {
this.setTo( newTo );
// Force to be vertical selection.
} else if ( deltaRow > deltaColumn ) {
this.setTo({
row: newTo.row,
column: this.from.column
});
// Force to be horizontal selection.
} else {
this.setTo({
row: this.from.row,
column: newTo.column
});
}
}
// ---
// PRIVATE METHODS.
// ---
// I gather all the concrete grid locations between the FROM and TO locations.
private gatherLocations() : GridLocation[] {
var count = Math.max(
( Math.abs( this.to.row - this.from.row ) + 1 ),
( Math.abs( this.to.column - this.from.column ) + 1 )
);
var rowIncrement = this.getIncrement( this.from.row, this.to.row );
var columnIncrement = this.getIncrement( this.from.column, this.to.column );
var iRow = this.from.row;
var iColumn = this.from.column;
var locations = [];
for ( var i = 0 ; i < count ; i++ ) {
locations.push({
row: iRow,
column: iColumn
});
iRow += rowIncrement;
iColumn += columnIncrement;
}
return( locations );
}
// I get the increment [-1, 0, 1] that can be used to loop over the given range.
private getIncrement( fromValue: number, toValue: number ) : number {
if ( fromValue < toValue ) {
return( 1 );
} else if ( fromValue > toValue ) {
return( -1 );
} else {
return( 0 );
}
}
// I set the starting location of the selection.
private setFrom( from: GridLocation ) : void {
this.from = this.to = Object.assign( {}, from );
}
// I set the ending location of the selection.
private setTo( to: GridLocation ) : void {
if (
// Not horizontal.
( this.from.row !== to.row ) &&
// Not vertical.
( this.from.column !== to.column ) &&
// Not diagonal.
( Math.abs( to.row - this.from.row ) !== Math.abs( to.column - this.from.column ) )
) {
throw( new Error( "InvalidSelection" ) );
}
this.to = Object.assign( {}, to );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment