Created
June 3, 2017 15:56
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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 ] ); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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